diff --git a/roles/airconnect/defaults/main.yml b/roles/airconnect/defaults/main.yml
index 710f734..653005a 100644
--- a/roles/airconnect/defaults/main.yml
+++ b/roles/airconnect/defaults/main.yml
@@ -8,6 +8,8 @@ airconnect_group:
name: airconnect
gid: 1337
+# for flac: 'flc'
+airconnect_codec: mp3:320
airconnect_max_volume: "100"
airconnect_upnp: []
diff --git a/roles/airconnect/templates/airupnp.xml.j2 b/roles/airconnect/templates/airupnp.xml.j2
index 9b64590..e45ee46 100644
--- a/roles/airconnect/templates/airupnp.xml.j2
+++ b/roles/airconnect/templates/airupnp.xml.j2
@@ -11,7 +11,7 @@
{{ airconnect_max_volume }}
-1
1
- mp3:320
+ {{ airconnect_codec }}
1
1
@@ -28,7 +28,9 @@
0:0
{% for item in airconnect_upnp -%}
+ {% if 'local_uuid' in item -%}
uuid:{{ item.local_uuid }}
+ {% endif -%}
{{ item.name }}
{% if 'mac' in item -%}
{{ item.mac | upper }}
diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py
new file mode 100755
index 0000000..b5ac854
--- /dev/null
+++ b/roles/audiobookshelf/files/fix-podcast-date.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+
+import argparse
+from datetime import datetime
+import subprocess
+import json
+import sys
+import shutil
+import os
+import time
+
+from loguru import logger
+import dateutil.parser
+from dateutil.parser._parser import ParserError
+
+def delete_file(file_path):
+ try:
+ os.remove(file_path)
+ logger.debug(f"deleted: '{file_path}'")
+ except FileNotFoundError:
+ pass
+
+
+def replace_file(src, dest):
+ try:
+ logger.debug(f"src: {src}, dest: {dest}")
+ delete_file(dest)
+ shutil.move(src, dest)
+ logger.debug(f"replaced '{dest}'")
+ except (PermissionError, OSError, FileNotFoundError) as e:
+ logger.error(e)
+ raise SystemExit(2)
+
+
+def ffprobe_get_tags(file_path):
+ cmd = [
+ "ffprobe",
+ "-v", "quiet",
+ file_path,
+ "-print_format", "json",
+ "-show_entries",
+ "stream_tags:format_tags"
+ ]
+ try:
+ p = subprocess.run(cmd, capture_output=True, check=True)
+ j = json.loads(p.stdout)
+ return j['format']['tags']
+ except subprocess.CalledProcessError as e:
+ logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}")
+ raise SystemExit(e.returncode)
+ except KeyError as e:
+ logger.error(f"key {e} for file '{file_path}' not found in ffprobe stdout: {p.stdout.decode()}")
+ raise SystemExit(2)
+
+
+def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str):
+ delete_file(out_file)
+
+ cmd = [
+ "ffmpeg",
+ "-nostdin",
+ "-y", # overwrite output file
+ "-i", podcast_file,
+ #"-c", "copy",
+ "-metadata", f"date={iso_date_str}",
+ #"-metadata", f"releasedate={iso_date_str}",
+ out_file
+ ]
+
+ try:
+ p = subprocess.run(cmd, capture_output=True, check=True, stdin=None)
+ p.check_returncode()
+ logger.debug(f"output: '{out_file}'")
+ replace_file(out_file, podcast_file)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}")
+ raise SystemExit(e.returncode)
+ finally:
+ delete_file(out_file)
+
+
+def eyeD3_write_date_tag(podcast_file, iso_date_str):
+
+ # import eyeD3 ?
+
+ podcast_dir = os.path.basename(podcast_file)
+ cover_path = os.path.join(podcast_dir, "cover.jpg")
+
+ cmd = [
+ "eyeD3",
+ "--release-date", iso_date_str,
+ "--orig-release-date", iso_date_str,
+ "--recording-date", iso_date_str,
+ # this overwrites 'release date' i think:
+ #"--release-year", iso_date_str.split("-")[0],
+ #"--preserve-file-times"
+ ]
+ # if os.path.exists(cover_path):
+ # cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"])
+ cmd.append(podcast_file)
+
+ logger.debug(" ".join(cmd))
+
+ try:
+ subprocess.run(cmd, capture_output=True, check=True, stdin=None)
+ logger.debug(f"updated: '{podcast_file}'")
+ except subprocess.CalledProcessError as e:
+ logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}")
+ raise SystemExit(e.returncode)
+
+
+def get_podcast_name_from_dir(podcast_file):
+ podcast_dir = os.path.dirname(podcast_file)
+ if podcast_dir.startswith("/"):
+ # for now lets just do absolute dirs for names
+ podcast_name = os.path.basename(podcast_dir)
+ logger.debug(f"podcast name: {podcast_name}")
+ return podcast_name
+ else:
+ return None
+
+
+def eyeD3_write_album_tag(podcast_file, podcast_name):
+ # "album" is the name of the podcast
+
+ cmd = ["eyeD3", "--album", podcast_name, podcast_file]
+ try:
+ subprocess.run(cmd, capture_output=True, check=True, stdin=None)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"{cmd[0]} exited with returncode {e.returncode} \n{e.stderr.decode()}")
+ raise SystemExit(e.returncode)
+
+
+def parse_iso_date(date_str):
+ try:
+ dt = dateutil.parser.parse(date_str)
+ return dt.date().isoformat()
+ except (ParserError, TypeError) as e:
+ logger.warning(f"invalid date string: '{date_str}'")
+
+
+def parse_TDAT_tag(tag_tdat):
+ try:
+ iso_date_str = tag_tdat.split(' ')[0]
+ return parse_iso_date(iso_date_str)
+ except (AttributeError, IndexError) as e:
+ logger.debug(f"invalid 'TDAT' tag: '{tag_tdat}'")
+ return None
+
+
+def get_iso_date_in_file(file_path):
+ tags = ffprobe_get_tags(file_path)
+
+ tag_TDAT = tags.get("TDAT")
+ tag_date = tags.get("date")
+
+ parsed_TDAT = parse_TDAT_tag(tag_TDAT)
+ parsed_date = parse_iso_date(tag_date)
+
+ if parsed_TDAT is None and parsed_date is None:
+ logger.error(f"no valid date found in '{file_path}' - TDAT: '{tag_TDAT}', date: '{tag_date}'")
+ raise SystemExit(3)
+
+ else:
+ logger.debug(f"TDAT: '{parsed_TDAT}' ('{tag_TDAT}'), date: '{parsed_date}' ('{tag_date}')")
+ logger.debug(f"date: {parsed_date}")
+
+ if parsed_TDAT != parsed_date:
+ logger.debug(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!")
+
+ if parsed_date is not None:
+ return parsed_date
+ else:
+ return parsed_TDAT
+
+
+def file_dates_are_ok(file_path):
+ tags = ffprobe_get_tags(file_path)
+ tag_date = tags.get("date")
+ try:
+ dt = datetime.fromisoformat(tag_date)
+ ts = time.mktime(dt.timetuple())
+ os.stat(file_path).st_mtime == ts
+ except ValueError:
+ return False
+
+def set_utime(file_path, iso_date_str):
+ dt = dateutil.parser.parse(iso_date_str)
+ ts = time.mktime(dt.timetuple())
+ # shutil.move(file_path, f"{file_path}.new")
+ # shutil.move(f"{file_path}.new", file_path)
+ os.utime(file_path, (ts, ts))
+ try:
+ os.utime(os.path.dirname(file_path), (ts, ts))
+ except FileNotFoundError:
+ pass
+ return dt
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("podcast_file")
+ parser.add_argument("--out-file", default="/tmp/out-{os.getpid()}.mp3")
+ parser.add_argument("--debug", action="store_true")
+ parser.add_argument("--quiet", action="store_true")
+ parser.add_argument("--ffmpeg", action="store_true")
+ parser.add_argument("--mtime", action="store_true", help="only set mtime, no mp3 metadata")
+ parser.add_argument("--fix-album-tag", action="store_true", help="write album tag (podcast name)")
+ parser.add_argument("--podcast-name")
+ args = parser.parse_args()
+
+ if not args.debug:
+ logger.remove()
+
+ if args.quiet:
+ logger.add(sys.stderr, level="ERROR")
+ else:
+ logger.add(sys.stderr, level="INFO")
+
+ return args
+
+
+def main():
+ args = parse_args()
+ logger.debug(f"checking: '{os.path.basename(args.podcast_file)}'")
+
+ if args.fix_album_tag:
+ podcast_name = get_podcast_name_from_dir(args.podcast_file)
+ if podcast_name is not None:
+ eyeD3_write_album_tag(args.podcast_file, podcast_name)
+ logger.info(f"set album tag to '{podcast_name}' for '{args.podcast_file}'")
+
+
+ date = get_iso_date_in_file(args.podcast_file)
+
+ if args.mtime:
+ dt = set_utime(args.podcast_filen, date)
+ logger.info(f"set mtime for '{os.path.basename(args.podcast_file)}' to '{dt.isoformat()}' according to mp3 metadata")
+
+ elif file_dates_are_ok(args.podcast_file):
+ logger.info(f"metadata date and filesystem utimes ar ok for {args.podcast_file}', did not modify file")
+ else:
+ if args.ffmpeg:
+ ffmpeg_write_date_tag(args.podcast_file, args.out_file, date)
+ else:
+ eyeD3_write_date_tag(args.podcast_file, date)
+
+ set_utime(args.podcast_file, date)
+ logger.success(f"updated date in '{args.podcast_file}' as {date}")
+
+
+
+if __name__ == "__main__":
+ main()
diff --git a/roles/audiobookshelf/files/releasedate.sh b/roles/audiobookshelf/files/releasedate.sh
new file mode 100755
index 0000000..cffa7cb
--- /dev/null
+++ b/roles/audiobookshelf/files/releasedate.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+
+set -e
+
+orig_file="$1"
+release_date="$2"
+
+if [[ "$orig_file" == "" ]]; then
+ echo "missing parameter"
+ exit 1
+fi
+
+if [[ "$release_date" == "" ]]; then
+ echo "missing parameter"
+ exit 1
+fi
+
+if [[ "$3" == "" ]]; then
+ prefix=""
+else
+ prefix="${3}-"
+fi
+
+orig_name=$(basename "$1")
+orig_dir=$(dirname "$1")
+cover_path="${orig_dir}/cover.jpg"
+
+ffmpeg_file="ffmpeg-${prefix}${orig_name}"
+eyed3_file="eyeD3-${prefix}${orig_name}"
+abs_podcast_dir="/deadspace/audiobookshelf/podcasts/00-test-metadata"
+
+rm "${ffmpeg_file}" &> /dev/null || true
+rm "no-date-tag-${ffmpeg_file}" &> /dev/null || true
+rm "releasedate-${ffmpeg_file}" &> /dev/null || true
+rm "release_date-${ffmpeg_file}" &> /dev/null || true
+rm "${eyed3_file}" &> /dev/null || true
+
+rm -vf "${abs_podcast_dir}/${ffmpeg_file}"
+rm -vf "${abs_podcast_dir}/${eyed3_file}"
+rm -vf "${abs_podcast_dir}/metadata.abs"
+rm -vf "${abs_podcast_dir}/cover.jpg"
+ls -l "${abs_podcast_dir}/"
+
+cp "${orig_file}" "${eyed3_file}"
+
+
+echo
+set -x
+eyeD3 --add-image "${cover_path}:FRONT_COVER" --release-date "${release_date}" "${eyed3_file}" > /dev/null
+
+#ffmpeg -i "${orig_file}" -c copy -metadata date="${release_date}" -metadata releasedate="${release_date}" "${ffmpeg_file}" &> /dev/null
+ffmpeg -i "${orig_file}" -c copy -metadata date="${release_date}" "${ffmpeg_file}" &> /dev/null
+#ffmpeg -i "${orig_file}" -c copy -metadata releasedate="${release_date}" "releasedate-${ffmpeg_file}" &> /dev/null
+#ffmpeg -i "${orig_file}" -c copy -metadata release_date="${release_date}" "release_date-${ffmpeg_file}" &> /dev/null
+set +x
+
+echo
+echo "--------"
+echo
+
+echo "file: ${orig_name}"
+echo "original as downloaded by abs"
+
+echo
+echo "> ffprobe ${orig_file}"
+ffprobe "${orig_file}" 2>&1 | grep date
+echo
+echo "> eyeD3 ${orig_file}"
+eyeD3 "${orig_file}" 2>&1 | grep date
+echo
+
+
+echo
+echo "--------"
+echo
+
+echo "file: ${eyed3_file}"
+echo "eyeD3 with '--release-date ${release_date}'"
+
+echo
+echo "> ffprobe ${eyed3_file}"
+ffprobe "${eyed3_file}" 2>&1 | egrep "image|date|COVER"
+echo
+echo "> eyeD3 ${eyed3_file}"
+eyeD3 "${eyed3_file}" 2>&1 | egrep "image|date|COVER"
+echo
+
+echo
+echo "--------"
+echo
+
+echo "file: ${ffmpeg_file}"
+#echo "ffmpeg with ONLY 'date=${release_date}'"
+echo "ffmpeg with BOTH 'date=${release_date}' and 'releasedate=${release_date}' set to ISO date strings"
+
+echo
+echo "> ffprobe ${ffmpeg_file}"
+ffprobe "${ffmpeg_file}" 2>&1 | grep date
+echo
+echo "> eyeD3 ${ffmpeg_file}"
+eyeD3 "${ffmpeg_file}" 2>&1 | grep date
+echo
+
+echo
+echo "--------"
+echo
+
+
+# echo
+# echo "--------"
+# echo
+# echo "file: releasedate-${ffmpeg_file}"
+# echo "ffmpeg with ONLY 'releasedate=${release_date}'"
+
+# echo
+# echo "> ffprobe releasedate-${ffmpeg_file}"
+# ffprobe "releasedate-${ffmpeg_file}" 2>&1 | grep date
+# echo
+# echo "> eyeD3 releasedate-${ffmpeg_file}"
+# eyeD3 "releasedate-${ffmpeg_file}" 2>&1 | grep date
+# echo
+
+# echo
+# echo "--------"
+# echo
+
+# echo "file: release_date-${ffmpeg_file}"
+# echo "ffmpeg with ONLY 'release_date=${release_date}'"
+
+# echo
+# echo "> ffprobe release_date-${ffmpeg_file}"
+# ffprobe "release_date-${ffmpeg_file}" 2>&1 | grep date
+# echo
+# echo "> eyeD3 release_date-${ffmpeg_file}"
+# eyeD3 "release_date-${ffmpeg_file}" 2>&1 | grep date
+# echo
+
+
+
+chgrp media "${eyed3_file}"
+chgrp media "${ffmpeg_file}"
+
+# chgrp media "releasedate-${ffmpeg_file}"
+# chgrp media "release_date-${ffmpeg_file}"
+
+
+
+set -x
+mv "${eyed3_file}" "${abs_podcast_dir}/"
+mv "${ffmpeg_file}" "${abs_podcast_dir}/"
+
+#mv "releasedate-${ffmpeg_file}" "${abs_podcast_dir}/"
+#mv "release_date-${ffmpeg_file}" "${abs_podcast_dir}/"
+set +x
+
+echo
+echo "> ls '${abs_podcast_dir}/'"
+ls -1 "${abs_podcast_dir}/"
+echo
+echo
+echo "done"
+
+
+# curl -si 'https://audio.sudo.is/api/rescan' -X PUT \
+# -H 'Accept: application/json, text/plain, */*' \
+# -H 'Cookie: authelia_session=W^lyficYWGLOKCevu9qzsTsrFF4i!hA0' \
+# -H 'Content-Length: 0' \
+# -H 'TE: trailers' | head -n 1
diff --git a/roles/audiobookshelf/tasks/audiobookshelf.yml b/roles/audiobookshelf/tasks/audiobookshelf.yml
index c66e5f1..0889f83 100644
--- a/roles/audiobookshelf/tasks/audiobookshelf.yml
+++ b/roles/audiobookshelf/tasks/audiobookshelf.yml
@@ -8,6 +8,7 @@
group: "{{ audiobookshelf_group.gid }}"
tags:
- audiobookshelf-dirs
+ - abs-dirs
loop_control:
label: "{{ item.name }}"
with_items:
@@ -43,6 +44,7 @@
tags:
- nginx
- audiobookshelf-nginx
+ - abs-nginx
notify: reload nginx
- name: start audiobookshelf container
@@ -79,4 +81,18 @@
target: /metadata
tags:
- audiobookshelf-container
+ - abs-container
- docker-containers
+
+- name: copy abs scripts
+ copy:
+ src: "{{ item }}"
+ dest: "/usr/local/bin/{{ item }}"
+ owner: "{{ audiobookshelf_user.uid }}"
+ group: "{{ audiobookshelf_group.gid }}"
+ mode: 0755
+ with_items:
+ - fix-podcast-date.py
+ tags:
+ - abs-scripts
+ - audiobookshelf-scripts
diff --git a/roles/backup/files/restic-backups.py b/roles/backup/files/restic-backups.py
index 89b7b19..6313aff 100755
--- a/roles/backup/files/restic-backups.py
+++ b/roles/backup/files/restic-backups.py
@@ -105,7 +105,19 @@ def run_restic(repo_url, restic_args, dry_run, non_interactive):
return
# .run(env={}) is possible (but then child doesnt inherit parent env)
- return subprocess.run(restic_cmd, check=True)
+ try:
+ ps = subprocess.run(restic_cmd, check=True, capture_output=False)
+ # if ps.stdout is not None:
+ # summary = ps.stdout.decode().split('\n')[-7:]
+ # if backup:
+ # logger.success(f"{summary[2].strip()} ({summary[4]})")
+ except subprocess.CalledProcessError as e:
+ cmd = " ".join(e.cmd)
+ # e.stdout, e.strderr
+ if e.stderr is not None:
+ logger.error(e.stderr.decode())
+ logger.error(f"command '{cmd}' returned non-zero exit status {e.returncode}.")
+ raise SystemExit(e.returncode)
def find_nobackup(config):
# very interesting restic option:
diff --git a/roles/hass/files/packages/amp.yaml b/roles/hass/files/packages/amp.yaml
new file mode 100644
index 0000000..757bd3f
--- /dev/null
+++ b/roles/hass/files/packages/amp.yaml
@@ -0,0 +1,117 @@
+input_boolean:
+ amp_muted:
+ name: amp_muted
+
+input_text:
+ nad_c370_sonos:
+ name: NAD C370 input for Sonos
+ initial: disc
+ pattern: disc
+
+ nad_c370_apple_tv:
+ name: NAD C370 input for Apple TV
+ initial: cd
+ pattern: cd
+
+ nad_c370_tv:
+ name: NAD C370 input for TV
+ initial: cd
+ pattern: cd
+
+input_select:
+ amp_inputs:
+ name: amp_inputs
+ options:
+ - video
+ - disc
+ - cd
+ - aux
+ - tape1
+ - tape2
+
+script:
+ amp_on:
+ mode: single
+ sequence:
+ - service: switch.turn_on
+ target:
+ entity_id: switch.nad_c370
+
+ amp_off:
+ mode: single
+ sequence:
+ - service: switch.turn_off
+ target:
+ entity_id: switch.nad_c370
+
+ amp_mute_toggle:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ command: mute
+ device: nad_c370
+
+ amp_remote_select_input:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ command: input_{{ source }}
+ device: nad_c370
+
+ amp_select_input2:
+ mode: single
+ sequence:
+ - service: script.amp_remote_select_input
+ data:
+ soure: >-
+ {% set input_device = states('input_select.media_center_input') %}
+ {{ states('input_text.nad_c370' ~ input_device ) }}
+ amp_select_input:
+ mode: single
+ sequence:
+ - if:
+ - condition: state
+ entity_id: input_select.media_center_inputs
+ state: "tv"
+ then:
+ - service: input_boolean.amp_remote_select_input
+ data:
+ amp_input_source: cd
+ - if:
+ - condition: state
+ entity_id: input_select.media_center_inputs
+ state: "apple_tv"
+ then:
+ - service: input_boolean.amp_remtote_select_input
+ data:
+ amp_input_source: cd
+ - if:
+ - condition: state
+ entity_id: input_select.media_center_inputs
+ state: "sonos"
+ then:
+ - service: input_boolean.amp_remtote_select_input
+ data:
+ amp_input_source: disc
+
+
+media_player:
+ - platform: universal
+ name: NAD C370
+ universal_id: nad_c370
+ children: []
+ turn_on:
+ service: script.amp_on
+ turn_off:
+ service: script.amp_off
+
+ select_source:
+ service: script.amp_select_input
+
+ attributes: state_attr('input_select.amp_inputs', 'options') | source_list
diff --git a/roles/hass/files/packages/fans.yaml b/roles/hass/files/packages/fans.yaml
new file mode 100644
index 0000000..b873d37
--- /dev/null
+++ b/roles/hass/files/packages/fans.yaml
@@ -0,0 +1,132 @@
+input_boolean:
+ standing_fan_1_state_power:
+ name: standing_fan_1_state_power
+ icon: mdi:fan
+ # no 'initial' value: restores to previous state on restart
+ #initial: false
+
+input_select:
+ standing_fan_1_preset_mode:
+ name: standing_fan_1_preset_mode
+ options:
+ - silent_night
+ - turbo
+ - normal
+ # no 'initial' value: restores to previous state on restart
+ #initial: normal
+ icon: mdi:fan
+
+script:
+ standing_fan_1_power_toggle:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: standing_fan_1
+ command: power_toggle
+
+ standing_fan_1_on:
+ mode: single
+ sequence:
+ - if:
+ - condition: state
+ entity_id: input_boolean.standing_fan_1_state_power
+ state: "off"
+ then:
+ - service: script.standing_fan_1_power_toggle
+ - service: input_boolean.turn_on
+ data: {}
+ target:
+ entity_id: input_boolean.standing_fan_1_state_power
+
+ standing_fan_1_off:
+ mode: single
+ sequence:
+ - if:
+ - condition: state
+ entity_id: input_boolean.standing_fan_1_state_power
+ state: "on"
+ then:
+ - service: script.standing_fan_1_power_toggle
+ - service: input_boolean.turn_off
+ data: {}
+ target:
+ entity_id: input_boolean.standing_fan_1_state_power
+
+ standing_fan_1_mode_silent_night:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: standing_fan_1
+ command: silent_night
+
+ standing_fan_1_mode_turbo:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: standing_fan_1
+ command: turbo
+
+ standing_fan_1_mode_normal:
+ mode: single
+ sequence:
+ # send the command for the active (if it is active) mode to turn it off (or back to 'normal')
+ - service: script.standing_fan_1_mode_{{ states('input_select.standing_fan_1_preset_mode') }}
+
+ standing_fan_1_reset_power_state:
+ mode: single
+ sequence:
+ - service: input_boolean.toggle
+ target:
+ entity_id: input_boolean.standing_fan_1_state_power
+ data: {}
+
+ standing_fan_1_set_preset_mode:
+ mode: single
+ sequence:
+ - service: script.standing_fan_1_mode_{{ preset_mode }}
+ - service: input_select.select_option
+ target:
+ entity_id: input_select.standing_fan_1_preset_mode
+ data:
+ option: "{{ preset_mode }}"
+
+# automation:
+# - alias: standing_fan_1_hass_shutdown
+# mode: single
+# trigger:
+# - platform: homeassistant
+# event: shutdown
+# condition: []
+# action:
+# - service: script.standing_fan_1_off
+
+
+fan:
+ - platform: template
+ fans:
+ standing_fan_1:
+ unique_id: standing_fan_1
+ friendly_name: "Standing fan 1"
+ value_template: "{{ states('input_boolean.standing_fan_1_state_power') }}"
+ preset_mode_template: "{{ states('input_select.standing_fan_1_preset_mode') }}"
+ turn_on:
+ service: script.standing_fan_1_on
+ turn_off:
+ service: script.standing_fan_1_off
+ set_preset_mode:
+ service: script.standing_fan_1_set_preset_mode
+ data:
+ preset_mode: "{{ preset_mode }}"
+ preset_modes:
+ - silent_night
+ - turbo
+ - normal
diff --git a/roles/hass/files/packages/grow_lights.yaml b/roles/hass/files/packages/grow_lights.yaml
new file mode 100644
index 0000000..23a14bc
--- /dev/null
+++ b/roles/hass/files/packages/grow_lights.yaml
@@ -0,0 +1,209 @@
+input_boolean:
+ grow_lights_1_state:
+ name: grow_lights_1_state
+ icon: mdi:sprout
+
+input_number:
+ grow_lights_1_brightness:
+ name: grow_lights_1_brightness
+ min: 1
+ max: 8
+ step: 1.0
+ initial: 8
+ mode: slider
+ icon: mdi:sprout
+
+
+script:
+ grow_lights_1_on:
+ mode: single
+ sequence:
+ - service: input_boolean.turn_on
+ data: {}
+ target:
+ entity_id: input_boolean.grow_lights_1_state
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: power_on
+ num_repeats: 3
+ delay_secs: 0.1
+ hold_secs: 0.1
+ - service: script.grow_lights_1_brightness_max
+
+ grow_lights_1_off:
+ mode: single
+ sequence:
+ - service: input_boolean.turn_off
+ data: {}
+ target:
+ entity_id: input_boolean.grow_lights_1_state
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: power_off
+ num_repeats: 3
+ delay_secs: 0.1
+ hold_secs: 0.1
+
+ grow_lights_1_brightness_up:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: brightness_up
+ delay_secs: 0.1
+ hold_secs: 0.1
+ num_repeats: "{{ num_repeats | default(1) | int | abs }}"
+
+ grow_lights_1_brightness_down:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: brightness_down
+ delay_secs: 0.1
+ hold_secs: 0.1
+ num_repeats: "{{ num_repeats | default(1) | int | abs }}"
+
+ grow_lights_1_brightness_min:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: brightness_down
+ hold_secs: 0.1
+ num_repeats: "{{ 8*2 | int }}"
+ - service: input_number.set_value
+ target:
+ entity_id: input_number.grow_lights_1_brightness
+ data:
+ value: "{{ 1 | int }}"
+
+ grow_lights_1_brightness_max:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: grow_lights_1
+ command: brightness_up
+ hold_secs: 0.1
+ num_repeats: "{{ 8*2 | int }}"
+ - service: input_number.set_value
+ target:
+ entity_id: input_number.grow_lights_1_brightness
+ data:
+ value: "{{ 8 | int }}"
+
+ grow_lights_1_brightness_set:
+ mode: single
+ sequence:
+ - service: script.grow_lights_1_brightness_min
+ - service: script.grow_lights_1_brightness_up
+ data:
+ num_repeats: "{{ brightness_steps | int }}"
+ - service: input_number.set_value
+ target:
+ entity_id: input_number.grow_lights_1_brightness
+ data:
+ value: "{{ brightness_steps | int }}"
+
+automation:
+ - alias: grow_lights_1_state
+ trigger:
+ - platform: time_pattern
+ minutes: /15
+ condition: []
+ action:
+ - service: >-
+ script.grow_lights_1_{{ states('binary_sensor.grow_lights_1_schedule') }}
+ data: {}
+
+ - alias: grow_lights_1_hass_start
+ trigger:
+ - platform: homeassistant
+ event: start
+ condition: []
+ action:
+ - service: >-
+ light.turn_{{ states('binary_sensor.grow_lights_1_schedule') }}
+ target:
+ entity_id: light.grow_lights_1
+ data: {}
+
+ - alias: grow_lights_1_schedule
+ trigger:
+ - platform: state
+ entity_id: binary_sensor.grow_lights_1_schedule
+ to: "on"
+ - platform: state
+ entity_id: binary_sensor.grow_lights_1_schedule
+ to: "off"
+ condition: []
+ action:
+ - service: >-
+ light.turn_{{ trigger.to_state.state }}
+ target:
+ entity_id: light.grow_lights_1
+ data: {}
+
+
+binary_sensor:
+ - platform: tod
+ unique_id: grow_lights_1_schedule
+ name: grow_lights_1_schedule
+ after: "09:00"
+ before: "19:00"
+
+
+light:
+ - platform: template
+ lights:
+ grow_lights_1:
+ unique_id: grow_lights_1
+ friendly_name: "Grow lights 1"
+ value_template: "{{ states('input_boolean.grow_lights_1_state') }}"
+ level_template: >-
+ {% set brightness_max_8 = states('input_number.grow_lights_1_brightness')|int %}
+ {% set brightness_max_256 = brightness_max_8 * 32 %}
+ {% set brightness_max_255 = min(255, brightness_max_256) %}
+ {{ max(1, brightness_max_255) }}
+ icon_template: >-
+ {% if is_state(this.entity_id, 'on') %}
+ mdi:sprout
+ {% else %}
+ mdi:sprout-outline
+ {% endif %}
+ turn_on:
+ service: script.grow_lights_1_on
+ turn_off:
+ service: script.grow_lights_1_off
+ set_level:
+ service: >-
+ {% if brightness > 200 %}
+ script.grow_lights_1_brightness_max
+ {% elif brightness < 100 %}
+ script.grow_lights_1_brightness_min
+ {% else %}
+ script.grow_lights_1_brightness_set
+ {% endif %}
+ data:
+ brightness: "{{ brightness | int }}"
+ brightness_steps: >-
+ {% set steps = brightness // 32 | default(8) | round | int %}
+ {{ max(1, steps) | int }}
diff --git a/roles/hass/files/packages/media_center.yaml b/roles/hass/files/packages/media_center.yaml
new file mode 100644
index 0000000..f7936f5
--- /dev/null
+++ b/roles/hass/files/packages/media_center.yaml
@@ -0,0 +1,123 @@
+input_boolean:
+ tv_assumed_state:
+ name: tv_assumed_power_state
+ icon: mdi:television
+
+
+input_select:
+ media_center_inputs:
+ name: media_center_inputs
+ options:
+ - sonos
+ - apple_tv
+ - tv
+
+script:
+ tv_power_toggle:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ command: power_toggle
+ device: tv
+
+ tv_on:
+ mode: single
+ sequence:
+ - if:
+ - condition: state
+ entity_id: input_boolean.tv_assumed_power_state
+ state: "off"
+ then:
+ - service: script.tv_power_toggle
+ - service: input_boolean.turn_on
+ target:
+ entity_id: input_boolean.tv_assumed_power_state
+
+ tv_off:
+ mode: single
+ sequence:
+ - if:
+ - condition: state
+ entity_id: input_boolean.tv_assumed_power_state
+ state: "on"
+ then:
+ - service: script.tv_power_toggle
+ - service: input_boolean.turn_off
+ target:
+ entity_id: input_boolean.tv_assumed_power_state
+
+ apple_tv_on:
+ mode: single
+ sequence:
+ # - service: homeassistant.reload_config_entry
+ # target:
+ # entity_id:
+ # #- media_player.apple_tv
+ # - remote.apple_tv
+ - service: remote.send_command
+ data:
+ command:
+ - wakeup
+ - select
+ delay_secs: 1
+ target:
+ entity_id: media_player.apple_tv
+
+ apple_tv_off:
+ mode: single
+ sequence:
+ - service: remote.send_command
+ data:
+ command:
+ - home_hold
+ - select
+ delay_secs: 1
+ target:
+ entity_id: media_player.apple_tv
+ # - service: homeassistant.reload_config_entry
+ # target:
+ # entity_id:
+ # #- media_player.apple_tv
+ # - remote.apple_tv
+
+
+ media_center_select_input:
+ mode: single
+ sequence:
+ - service: input_select.select_option
+ target:
+ entity_id: input_boolean.media_center_inputs
+ data:
+ option: "{{ media_center_input }}"
+ - service: script.amp_select_input
+ # another one for tv?
+
+
+ media_center_on:
+ mode: single
+ sequence:
+ - service: script.amp_on
+ - service: script.tv_on
+ - service: script.apple_tv_on
+
+ media_center_off:
+ mode: single
+ sequence:
+ - service: script.amp_off
+ - service: script.tv_off
+ - service: script.apple_tv_off
+ - service: media_player.turn_off
+ target:
+ entity_id: media_player.owntone_output_living_room
+
+media_player:
+ - platform: universal
+ name: Media Center
+ universal_id: media_center
+ children:
+ - media_player.apple_tv
+ - media_player.living_room_audio
+ - media_player.owntone_server
diff --git a/roles/hass/files/packages/usb_led_strings.yaml b/roles/hass/files/packages/usb_led_strings.yaml
new file mode 100644
index 0000000..a695c5c
--- /dev/null
+++ b/roles/hass/files/packages/usb_led_strings.yaml
@@ -0,0 +1,88 @@
+input_boolean:
+ usb_led_string_1_state:
+ name: usb_led_string_1_state
+ initial: false
+
+script:
+ usb_led_string_1_on:
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: usb_led_string_1
+ command:
+ - power_on
+ - steady_on
+ - service: input_boolean.turn_on
+ data: {}
+ target:
+ entity_id: input_boolean.usb_led_string_1_state
+
+ usb_led_string_1_off:
+ sequence:
+ - service: remote.send_command
+ target:
+ entity_id: remote.broadlink
+ data:
+ device: usb_led_string_1
+ command:
+ - power_off
+ - service: input_boolean.turn_off
+ data: {}
+ target:
+ entity_id: input_boolean.usb_led_string_1_state
+
+automation:
+ - alias: usb_led_string_1_state
+ trigger:
+ - platform: state
+ entity_id: input_boolean.usb_led_string_1_state
+ for:
+ hours: 0
+ minutes: 5
+ seconds: 0
+ to: "on"
+ id: "on"
+ - platform: state
+ entity_id: input_boolean.usb_led_string_1_state
+ for:
+ hours: 0
+ minutes: 5
+ seconds: 0
+ to: "off"
+ id: "off"
+ condition: []
+ action:
+ - service: >-
+ script.usb_led_string_1_{{ trigger.id }}
+ data: {}
+
+ - alias: usb_led_string_1_hass_start
+ trigger:
+ - platform: homeassistant
+ event: start
+ condition: []
+ action:
+ - service: >-
+ {% if is_state('sun.sun', 'below_horizon') %}
+ light.turn_on
+ {% else %}
+ light.turn_off
+ {% endif %}
+ target:
+ entity_id: light.usb_led_string_1
+ data: {}
+
+light:
+ - platform: template
+ lights:
+ usb_led_string_1:
+ unique_id: usb_led_string_1
+ friendly_name: "USB LED string 1"
+ value_template: "{{ states('input_boolean.usb_led_string_1_state') }}"
+ icon_template: mdi:led-strip-variant
+ turn_on:
+ service: script.usb_led_string_1_on
+ turn_off:
+ service: script.usb_led_string_1_off
diff --git a/roles/hass/tasks/hass.yml b/roles/hass/tasks/hass.yml
index d0f93b9..bb0b098 100644
--- a/roles/hass/tasks/hass.yml
+++ b/roles/hass/tasks/hass.yml
@@ -32,6 +32,7 @@
mode: "0775"
- name: home-assistant
- name: home-assistant/config
+ - name: home-assistant/config/packages
- name: home-assistant/config/python_scripts
- name: home-assistant/config/bvg
- name: home-assistant/.config # might not be needed, misread 'cache' as 'config'
@@ -68,7 +69,6 @@
- hass-git
- hass-git-clone
-
- name: home assistant config files
template:
src: "{{ item }}.j2"
@@ -81,20 +81,36 @@
- secrets.yaml
- configuration.yaml
- templates.yaml
- - climate.yaml
- automations-ansible-managed.yaml
- scripts-ansible-managed.yaml
- blink1.yaml
+ # - packages/climate.yaml
+ - packages/toothbrush.yaml
tags:
- hass-config
-- name: copy dashboards
+- name: copy config files
+ copy:
+ src: "{{ item }}"
+ dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}"
+ mode: 0644
+ owner: "{{ systemuserlist.hass.uid }}"
+ group: "{{ systemuserlist.hass.gid }}"
+ notify: restart hass container
+ with_items:
+ - packages/usb_led_strings.yaml
+ - packages/grow_lights.yaml
+ - packages/fans.yaml
+ tags:
+ - hass-config
+
+- name: copy dashboard config files
copy:
src: "private/hass/{{ item }}"
dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}"
- mode: 0755
- owner: hass
- group: hass
+ mode: 0644
+ owner: "{{ systemuserlist.hass.uid }}"
+ group: "{{ systemuserlist.hass.gid }}"
notify: restart hass container
with_items:
- mini.yaml
@@ -200,6 +216,7 @@
- mplayer
- bluez
- bluetooth
+ - espeak-ng # for glados-tts
- fapg
- podget
- sqlite3
@@ -281,8 +298,8 @@
- name: start home-assistant container
docker_container:
name: hass
- image: ghcr.io/home-assistant/home-assistant:stable
- #image: git.sudo.is/ben/hass:latest
+ #image: ghcr.io/home-assistant/home-assistant:stable
+ image: git.sudo.is/ben/hass:latest
detach: true
pull: true
restart_policy: "unless-stopped"
diff --git a/roles/hass/templates/automations-ansible-managed.yaml.j2 b/roles/hass/templates/automations-ansible-managed.yaml.j2
index 352282a..3186afd 100644
--- a/roles/hass/templates/automations-ansible-managed.yaml.j2
+++ b/roles/hass/templates/automations-ansible-managed.yaml.j2
@@ -23,6 +23,7 @@
title: 'device not reporting'
message: 'stopped reporting: {%raw%}{{ trigger.id }}{%endraw%}'
+{% if hass_light_switches | length > 0 -%}
- alias: refresh_light_switches_state
description: the tkbhome switches dont automatically report their state
trigger:
@@ -39,6 +40,7 @@
- {{ item.entity_id }}
{% endif -%}
{% endfor %}
+{% endif -%}
{% for item in hass_light_switches -%}
{% set domain = item.entity_id.split('.')[0] %}
diff --git a/roles/hass/templates/climate.yaml.j2 b/roles/hass/templates/climate.yaml.j2
index 897c2b3..7d13206 100644
--- a/roles/hass/templates/climate.yaml.j2
+++ b/roles/hass/templates/climate.yaml.j2
@@ -12,7 +12,6 @@
current_temperature_template: "{{ states('input_number.heating_setpoint_test') }}"
hvac_mode_template: "{{ states('input_select.heating_mode_test') }}"
current_humidity_template: 0.0
- swing_mode_template: false
availability_template: true
set_temperature:
diff --git a/roles/hass/templates/configuration.yaml.j2 b/roles/hass/templates/configuration.yaml.j2
index 70be9ea..15006e4 100644
--- a/roles/hass/templates/configuration.yaml.j2
+++ b/roles/hass/templates/configuration.yaml.j2
@@ -62,7 +62,7 @@ automation ansible: !include automations-ansible-managed.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
template: !include templates.yaml
-climate: !include climate.yaml
+#climate: !include climate.yaml
# Text to speech
tts:
@@ -89,6 +89,8 @@ http:
- 127.0.0.1
use_x_forwarded_for: true
+api:
+
frontend:
themes: !include_dir_merge_named themes
@@ -158,6 +160,7 @@ homeassistant:
customize:
zone.home:
friendly_name: S21
+ packages: !include_dir_named packages
lovelace:
mode: storage
@@ -228,6 +231,24 @@ sensor:
file_path: "/config/bvg/"
{% endfor %}
+ - platform: worldclock
+ time_zone: UTC
+ name: UTC
+ - platform: worldclock
+ time_zone: Atlantic/Reykjavik
+ name: Iceland
+ - platform: worldclock
+ time_zone: Europe/Berlin
+ name: Berlin
+ - platform: worldclock
+ time_zone: America/New_York
+ name: Boston
+ - platform: worldclock
+ time_zone: America/Chicago
+ name: Austin
+
+
+
- platform: waqi
token: !secret waqi_token
locations:
@@ -306,24 +327,33 @@ input_text:
device_tracker:
- platform: bluetooth_le_tracker
+ # device tracker will only look for the following global settings
+ # under the configuration of the first configured platform
interval_seconds: 12
- track_new_devices: false
- track_battery: false
- consider_home: 150
- new_device_defaults:
- track_new_devices: false
+ consider_home: 120
+ # setting this to 'false' still results in new devices being added
+ # to known_devices.yaml when they are discovered, but they wont be
+ # tracked unless 'track' is set to 'true' for a device there (edit
+ # the file to track a discovered device).
+ track_new_devices: true
+ #new_device_defaults:
+ # track_new_devices: false
+
- platform: bluetooth_tracker
- request_rssi: false
- interval_seconds: 12
- track_new_devices: false
- consider_home: 150
- new_device_defaults:
- track_new_devices: false
+ request_rssi: true
+ track_new_devices: true
- platform: ping
+ # these are probably not used, because they are "global settings"
+ # and only the first values from the first platform (bluetooth_le_tracker)
+ # are used according to the docs
+ interval_seconds: 30
+ track_new_devices: true
+ # but consider_hoem can be overridden
+ consider_home: 120
hosts:
{% for target in hass_ping -%}
{% if target.device_tracker|default(true) -%}
- {{ target.name }}: {{ target.host }}
+ ping_{{ target.name }}: {{ target.host }}
{% endif -%}
{% endfor %}
@@ -402,24 +432,19 @@ light:
blink1: !include blink1.yaml
{% endif %}
-# enable 'wake_on_lan' for 'samsungtv'
-wake_on_lan:
+{#
+ # feedreader:
+ # urls:
+ # {% for item in hass_feedreader -%}
+ # - "{{ item.url | trim }}"
+ # {% endfor %}
+ #}
-samsungtv:
- - host: {{ hass_wifi_blackbox.tv_ip }}
- name: The TV
- turn_on_action:
- - service: wake_on_lan.send_magic_packet
- data:
- mac: "{{ hass_wifi_blackbox.tv_mac }}"
+logger:
+ default: warning
+ logs:
+ pyatv: debug
+ homeassistant.components.apple_tv: debug
-feedreader:
- urls:
- {% for item in hass_feedreader -%}
- - "{{ item.url | trim }}"
- {% endfor %}
-
-{# logger:
- # logs:
- # pyatv: debug
- # homeassistant.components.apple_tv: debug #}
+# enable SVT play
+svt_play:
diff --git a/roles/hass/templates/git-hass-config.sh.j2 b/roles/hass/templates/git-hass-config.sh.j2
index 9d8b408..31c8215 100644
--- a/roles/hass/templates/git-hass-config.sh.j2
+++ b/roles/hass/templates/git-hass-config.sh.j2
@@ -25,13 +25,26 @@ fi
mkdir -p ${PATH_REPO}/config/
mkdir -p ${PATH_REPO}/config/.storage
-{% for item in hass_config_repo_files -%}
-cp ${PATH_HASS}/config/{{ item }} ${PATH_REPO}/config/{{ item }}
+{% for item in hass_config_repo_cp -%}
+{% if item.dir|default(false) %}{% set cp_r = "r" -%}
+{% else %}{% set cp_r = "" -%}
+{% endif -%}
+{% if item.src.endswith("*") -%}
+{% if item.src.count("/") > 0 -%}
+{% set dest = item.src.split("/")[:-1] | join("/") + "/" -%}
+{% else -%}
+{% set dest = "" -%}
+{% endif -%}
+{% else -%}
+{% set dest = item.src %}
+{% endif %}
+cp -a{{ cp_r }} ${PATH_HASS}/config/{{ item.src }} ${PATH_REPO}/config/{{ dest }}
{% endfor %}
+
if test -n "$(git status --porcelain)" ; then
- {% for item in hass_config_repo_files -%}
- git add config/{{ item }} > /dev/null
+ {% for item in hass_config_repo_cp -%}
+ git add config/{{ item.src }} > /dev/null
{% endfor %}
git commit -m "config updated" > /dev/null
diff --git a/roles/hass/templates/hass-cron.j2 b/roles/hass/templates/hass-cron.j2
index 2b08717..33c5241 100644
--- a/roles/hass/templates/hass-cron.j2
+++ b/roles/hass/templates/hass-cron.j2
@@ -5,7 +5,4 @@
# sync hass config to {{ hass_config_repo }}
*/15 * * * * {{ systemuserlist.hass.username }} /usr/local/bin/git-hass-config.sh
-
-
-
#
diff --git a/roles/hass/templates/packages/toothbrush.yaml.j2 b/roles/hass/templates/packages/toothbrush.yaml.j2
new file mode 100644
index 0000000..e2aaecd
--- /dev/null
+++ b/roles/hass/templates/packages/toothbrush.yaml.j2
@@ -0,0 +1,54 @@
+input_number:
+ toothbrushing_target_time:
+ name: toothbrushing_target_time
+ min: 10
+ max: 600
+ step: 1
+ initial: {{ toothbrushing_target_time }}
+ icon: "mdi:toothbrush-electric"
+ mode: box
+
+template:
+ - sensor:
+
+ {% for item in hass_toothbrushes -%}
+
+ - name: "{{ item.entity_name }}"
+ unique_id: "{{ item.entity_name }}"
+ icon: "mdi:toothbrush-electric"
+ state_class: "measurement"
+ unit_of_measurement: "s"
+ {% raw -%}
+ state: >-
+ {% set target_time = states('input_number.toothbrushing_target_time') | int %}
+ {% set source_entity_id = this.attributes.source_entity_id | default("unknown") %}
+ {% if states(source_entity_id) not in ["unknown", "unavailable"] %}
+ {% set time = states(source_entity_id) | int %}
+ {% endif %}
+ {% set brush_time = time|default(0)|int %}
+ {{ max(0, target_time - brush_time) }}
+ {% endraw -%}
+ attributes:
+ source_entity_id: "{{ item.source_entity_id }}"
+
+ {% endfor %}
+
+ - binary_sensor:
+
+ {% for item in hass_toothbrushes -%}
+
+ - name: "{{ item.entity_name }}"
+ unique_id: "{{ item.entity_name }}"
+ icon: "mdi:toothbrush-electric"
+ delay_off: "00:00:10"
+ {% raw -%}
+ state: >-
+ {{ states("sensor." ~ this.name)|int == 0 }}
+ attributes:
+ hours_since_last_brush: >-
+ {% set time_since = now() - this.last_changed %}
+ {{ time_since.seconds // 60 // 60 }}
+
+ {% endraw %}
+
+ {% endfor %}
diff --git a/roles/hass/templates/templates.yaml.j2 b/roles/hass/templates/templates.yaml.j2
index b4c7f0e..e41a0a3 100644
--- a/roles/hass/templates/templates.yaml.j2
+++ b/roles/hass/templates/templates.yaml.j2
@@ -1,3 +1,22 @@
+{#
+ # device attributes:
+ # - config_entries
+ # - connections
+ # - identifiers
+ # - manufacturer
+ # - model
+ # - name
+ # - sw_version
+ # - hw_version
+ # - entry_type
+ # - id
+ # - via_device_id
+ # - area_id
+ # - name_by_user
+ # - disabled_by
+ # - configuration_url
+ #}
+
- sensor:
- name: "chance_of_rain"
unit_of_measurement: "%"
@@ -14,7 +33,7 @@
{% if 'status' in radiator %}
- name: "radiator_{{ radiator.name }}_last_updated"
- unit_of_measurement: "minutes"
+ unit_of_measurement: "min"
icon: "mdi:update"
device_class: duration
state: >-
@@ -30,6 +49,22 @@
{% for linux_tracker in hass_linux_presence_trackers -%}
{% endfor %}
+
+
+ {# {% if is_state("sensor." ~ this.name ~ "_state", "unavailable") %}
+ # {{ false | bool }}
+ # {% elif is_state("sensor." ~ this.name ~ "_state", "unavailable") %}
+ # {{ false | bool }}
+ # {% elif is_state("sensor." ~ this.name ~ "_time", "unknown") %}
+ # {{ false | bool }}
+ # {% else %}
+ # {{ true | bool }}
+ #
+ # friendly_name: >-
+ # {{ device_attr("sensor." ~ this.name ~ "_state", "name_by_user") }}
+ #
+ #}
+
- binary_sensor:
{% if blink1_enabled -%}
- name: "blink1_on"
@@ -51,15 +86,6 @@
{{ max_radiator_temp >= target_temp }}
{% endraw %}
- - name: s21_anyone_home
- icon: "mdi:home-account"
- attributes:
- friendly_name: "Anyone home"
- state: >-
- {% raw -%}
- {{ state_attr("zone.home", "persons") | default([]) | length > 0 }}
- {% endraw %}
-
- name: doorbell_buzzer
state: >-
{% raw %} {{ is_state("switch.doorbell_buzzer", "on") }} {% endraw +%}
@@ -88,6 +114,7 @@
{% endif %}
{% endraw %}
+
{% for linux_tracker in hass_linux_presence_trackers -%}
- name: {{ linux_tracker.name }}_active
icon: "mdi:laptop"
diff --git a/roles/owntone/templates/owntone.conf.j2 b/roles/owntone/templates/owntone.conf.j2
index 390157a..3f6643c 100644
--- a/roles/owntone/templates/owntone.conf.j2
+++ b/roles/owntone/templates/owntone.conf.j2
@@ -224,12 +224,13 @@ audio {
nickname = "Computer"
# Type of the output (alsa, pulseaudio, dummy or disabled)
- type = "disabled"
+ #type = "disabled"
+ type = "pulseaudio"
# For pulseaudio output, an optional server hostname or IP can be
# specified (e.g. "localhost"). If not set, connection is made via local
# socket.
- #server = ""
+ server = "localhost"
# Audio PCM device name for local audio output - ALSA only
#card = "default"
@@ -379,7 +380,7 @@ spotify {
# Your Spotify playlists will by default be put in a "Spotify" playlist
# folder. If you would rather have them together with your other
# playlists you can set this option to true.
- base_playlist_disable = true
+ base_playlist_disable = false
# Spotify playlists usually have many artist, and if you dont want
# every artist to be listed when artist browsing in Remote, you can set