recent hass changes #57

Merged
ben merged 17 commits from hass into main 2023-05-15 11:55:00 +00:00
20 changed files with 1321 additions and 63 deletions

View File

@ -8,6 +8,8 @@ airconnect_group:
name: airconnect name: airconnect
gid: 1337 gid: 1337
# for flac: 'flc'
airconnect_codec: mp3:320
airconnect_max_volume: "100" airconnect_max_volume: "100"
airconnect_upnp: [] airconnect_upnp: []

View File

@ -11,7 +11,7 @@
<max_volume>{{ airconnect_max_volume }}</max_volume> <max_volume>{{ airconnect_max_volume }}</max_volume>
<http_length>-1</http_length> <http_length>-1</http_length>
<upnp_max>1</upnp_max> <upnp_max>1</upnp_max>
<codec>mp3:320</codec> <codec>{{ airconnect_codec }}</codec>
<metadata>1</metadata> <metadata>1</metadata>
<flush>1</flush> <flush>1</flush>
<artwork></artwork> <artwork></artwork>
@ -28,7 +28,9 @@
<ports>0:0</ports> <ports>0:0</ports>
{% for item in airconnect_upnp -%} {% for item in airconnect_upnp -%}
<device> <device>
{% if 'local_uuid' in item -%}
<udn>uuid:{{ item.local_uuid }}</udn> <udn>uuid:{{ item.local_uuid }}</udn>
{% endif -%}
<name>{{ item.name }}</name> <name>{{ item.name }}</name>
{% if 'mac' in item -%} {% if 'mac' in item -%}
<mac>{{ item.mac | upper }}</mac> <mac>{{ item.mac | upper }}</mac>

View File

@ -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()

View File

@ -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

View File

@ -8,6 +8,7 @@
group: "{{ audiobookshelf_group.gid }}" group: "{{ audiobookshelf_group.gid }}"
tags: tags:
- audiobookshelf-dirs - audiobookshelf-dirs
- abs-dirs
loop_control: loop_control:
label: "{{ item.name }}" label: "{{ item.name }}"
with_items: with_items:
@ -43,6 +44,7 @@
tags: tags:
- nginx - nginx
- audiobookshelf-nginx - audiobookshelf-nginx
- abs-nginx
notify: reload nginx notify: reload nginx
- name: start audiobookshelf container - name: start audiobookshelf container
@ -79,4 +81,18 @@
target: /metadata target: /metadata
tags: tags:
- audiobookshelf-container - audiobookshelf-container
- abs-container
- docker-containers - 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

View File

@ -105,7 +105,19 @@ def run_restic(repo_url, restic_args, dry_run, non_interactive):
return return
# .run(env={}) is possible (but then child doesnt inherit parent env) # .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): def find_nobackup(config):
# very interesting restic option: # very interesting restic option:

View File

@ -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

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -32,6 +32,7 @@
mode: "0775" mode: "0775"
- name: home-assistant - name: home-assistant
- name: home-assistant/config - name: home-assistant/config
- name: home-assistant/config/packages
- name: home-assistant/config/python_scripts - name: home-assistant/config/python_scripts
- name: home-assistant/config/bvg - name: home-assistant/config/bvg
- name: home-assistant/.config # might not be needed, misread 'cache' as 'config' - name: home-assistant/.config # might not be needed, misread 'cache' as 'config'
@ -68,7 +69,6 @@
- hass-git - hass-git
- hass-git-clone - hass-git-clone
- name: home assistant config files - name: home assistant config files
template: template:
src: "{{ item }}.j2" src: "{{ item }}.j2"
@ -81,20 +81,36 @@
- secrets.yaml - secrets.yaml
- configuration.yaml - configuration.yaml
- templates.yaml - templates.yaml
- climate.yaml
- automations-ansible-managed.yaml - automations-ansible-managed.yaml
- scripts-ansible-managed.yaml - scripts-ansible-managed.yaml
- blink1.yaml - blink1.yaml
# - packages/climate.yaml
- packages/toothbrush.yaml
tags: tags:
- hass-config - 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: copy:
src: "private/hass/{{ item }}" src: "private/hass/{{ item }}"
dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}" dest: "{{ systemuserlist.hass.home }}/home-assistant/config/{{ item }}"
mode: 0755 mode: 0644
owner: hass owner: "{{ systemuserlist.hass.uid }}"
group: hass group: "{{ systemuserlist.hass.gid }}"
notify: restart hass container notify: restart hass container
with_items: with_items:
- mini.yaml - mini.yaml
@ -200,6 +216,7 @@
- mplayer - mplayer
- bluez - bluez
- bluetooth - bluetooth
- espeak-ng # for glados-tts
- fapg - fapg
- podget - podget
- sqlite3 - sqlite3
@ -281,8 +298,8 @@
- name: start home-assistant container - name: start home-assistant container
docker_container: docker_container:
name: hass name: hass
image: ghcr.io/home-assistant/home-assistant:stable #image: ghcr.io/home-assistant/home-assistant:stable
#image: git.sudo.is/ben/hass:latest image: git.sudo.is/ben/hass:latest
detach: true detach: true
pull: true pull: true
restart_policy: "unless-stopped" restart_policy: "unless-stopped"

View File

@ -23,6 +23,7 @@
title: 'device not reporting' title: 'device not reporting'
message: 'stopped reporting: {%raw%}{{ trigger.id }}{%endraw%}' message: 'stopped reporting: {%raw%}{{ trigger.id }}{%endraw%}'
{% if hass_light_switches | length > 0 -%}
- alias: refresh_light_switches_state - alias: refresh_light_switches_state
description: the tkbhome switches dont automatically report their state description: the tkbhome switches dont automatically report their state
trigger: trigger:
@ -39,6 +40,7 @@
- {{ item.entity_id }} - {{ item.entity_id }}
{% endif -%} {% endif -%}
{% endfor %} {% endfor %}
{% endif -%}
{% for item in hass_light_switches -%} {% for item in hass_light_switches -%}
{% set domain = item.entity_id.split('.')[0] %} {% set domain = item.entity_id.split('.')[0] %}

View File

@ -12,7 +12,6 @@
current_temperature_template: "{{ states('input_number.heating_setpoint_test') }}" current_temperature_template: "{{ states('input_number.heating_setpoint_test') }}"
hvac_mode_template: "{{ states('input_select.heating_mode_test') }}" hvac_mode_template: "{{ states('input_select.heating_mode_test') }}"
current_humidity_template: 0.0 current_humidity_template: 0.0
swing_mode_template: false
availability_template: true availability_template: true
set_temperature: set_temperature:

View File

@ -62,7 +62,7 @@ automation ansible: !include automations-ansible-managed.yaml
script: !include scripts.yaml script: !include scripts.yaml
scene: !include scenes.yaml scene: !include scenes.yaml
template: !include templates.yaml template: !include templates.yaml
climate: !include climate.yaml #climate: !include climate.yaml
# Text to speech # Text to speech
tts: tts:
@ -89,6 +89,8 @@ http:
- 127.0.0.1 - 127.0.0.1
use_x_forwarded_for: true use_x_forwarded_for: true
api:
frontend: frontend:
themes: !include_dir_merge_named themes themes: !include_dir_merge_named themes
@ -158,6 +160,7 @@ homeassistant:
customize: customize:
zone.home: zone.home:
friendly_name: S21 friendly_name: S21
packages: !include_dir_named packages
lovelace: lovelace:
mode: storage mode: storage
@ -228,6 +231,24 @@ sensor:
file_path: "/config/bvg/" file_path: "/config/bvg/"
{% endfor %} {% 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 - platform: waqi
token: !secret waqi_token token: !secret waqi_token
locations: locations:
@ -306,24 +327,33 @@ input_text:
device_tracker: device_tracker:
- platform: bluetooth_le_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 interval_seconds: 12
track_new_devices: false consider_home: 120
track_battery: false # setting this to 'false' still results in new devices being added
consider_home: 150 # to known_devices.yaml when they are discovered, but they wont be
new_device_defaults: # tracked unless 'track' is set to 'true' for a device there (edit
track_new_devices: false # the file to track a discovered device).
track_new_devices: true
#new_device_defaults:
# track_new_devices: false
- platform: bluetooth_tracker - platform: bluetooth_tracker
request_rssi: false request_rssi: true
interval_seconds: 12 track_new_devices: true
track_new_devices: false
consider_home: 150
new_device_defaults:
track_new_devices: false
- platform: ping - 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: hosts:
{% for target in hass_ping -%} {% for target in hass_ping -%}
{% if target.device_tracker|default(true) -%} {% if target.device_tracker|default(true) -%}
{{ target.name }}: {{ target.host }} ping_{{ target.name }}: {{ target.host }}
{% endif -%} {% endif -%}
{% endfor %} {% endfor %}
@ -402,24 +432,19 @@ light:
blink1: !include blink1.yaml blink1: !include blink1.yaml
{% endif %} {% endif %}
# enable 'wake_on_lan' for 'samsungtv' {#
wake_on_lan: # feedreader:
# urls:
# {% for item in hass_feedreader -%}
# - "{{ item.url | trim }}"
# {% endfor %}
#}
samsungtv: logger:
- host: {{ hass_wifi_blackbox.tv_ip }} default: warning
name: The TV logs:
turn_on_action: pyatv: debug
- service: wake_on_lan.send_magic_packet homeassistant.components.apple_tv: debug
data:
mac: "{{ hass_wifi_blackbox.tv_mac }}"
feedreader: # enable SVT play
urls: svt_play:
{% for item in hass_feedreader -%}
- "{{ item.url | trim }}"
{% endfor %}
{# logger:
# logs:
# pyatv: debug
# homeassistant.components.apple_tv: debug #}

View File

@ -25,13 +25,26 @@ fi
mkdir -p ${PATH_REPO}/config/ mkdir -p ${PATH_REPO}/config/
mkdir -p ${PATH_REPO}/config/.storage mkdir -p ${PATH_REPO}/config/.storage
{% for item in hass_config_repo_files -%} {% for item in hass_config_repo_cp -%}
cp ${PATH_HASS}/config/{{ item }} ${PATH_REPO}/config/{{ item }} {% 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 %} {% endfor %}
if test -n "$(git status --porcelain)" ; then if test -n "$(git status --porcelain)" ; then
{% for item in hass_config_repo_files -%} {% for item in hass_config_repo_cp -%}
git add config/{{ item }} > /dev/null git add config/{{ item.src }} > /dev/null
{% endfor %} {% endfor %}
git commit -m "config updated" > /dev/null git commit -m "config updated" > /dev/null

View File

@ -5,7 +5,4 @@
# sync hass config to {{ hass_config_repo }} # sync hass config to {{ hass_config_repo }}
*/15 * * * * {{ systemuserlist.hass.username }} /usr/local/bin/git-hass-config.sh */15 * * * * {{ systemuserlist.hass.username }} /usr/local/bin/git-hass-config.sh
# #

View File

@ -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 %}

View File

@ -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: - sensor:
- name: "chance_of_rain" - name: "chance_of_rain"
unit_of_measurement: "%" unit_of_measurement: "%"
@ -14,7 +33,7 @@
{% if 'status' in radiator %} {% if 'status' in radiator %}
- name: "radiator_{{ radiator.name }}_last_updated" - name: "radiator_{{ radiator.name }}_last_updated"
unit_of_measurement: "minutes" unit_of_measurement: "min"
icon: "mdi:update" icon: "mdi:update"
device_class: duration device_class: duration
state: >- state: >-
@ -30,6 +49,22 @@
{% for linux_tracker in hass_linux_presence_trackers -%} {% for linux_tracker in hass_linux_presence_trackers -%}
{% endfor %} {% 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: - binary_sensor:
{% if blink1_enabled -%} {% if blink1_enabled -%}
- name: "blink1_on" - name: "blink1_on"
@ -51,15 +86,6 @@
{{ max_radiator_temp >= target_temp }} {{ max_radiator_temp >= target_temp }}
{% endraw %} {% 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 - name: doorbell_buzzer
state: >- state: >-
{% raw %} {{ is_state("switch.doorbell_buzzer", "on") }} {% endraw +%} {% raw %} {{ is_state("switch.doorbell_buzzer", "on") }} {% endraw +%}
@ -88,6 +114,7 @@
{% endif %} {% endif %}
{% endraw %} {% endraw %}
{% for linux_tracker in hass_linux_presence_trackers -%} {% for linux_tracker in hass_linux_presence_trackers -%}
- name: {{ linux_tracker.name }}_active - name: {{ linux_tracker.name }}_active
icon: "mdi:laptop" icon: "mdi:laptop"

View File

@ -224,12 +224,13 @@ audio {
nickname = "Computer" nickname = "Computer"
# Type of the output (alsa, pulseaudio, dummy or disabled) # 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 # For pulseaudio output, an optional server hostname or IP can be
# specified (e.g. "localhost"). If not set, connection is made via local # specified (e.g. "localhost"). If not set, connection is made via local
# socket. # socket.
#server = "" server = "localhost"
# Audio PCM device name for local audio output - ALSA only # Audio PCM device name for local audio output - ALSA only
#card = "default" #card = "default"
@ -379,7 +380,7 @@ spotify {
# Your Spotify playlists will by default be put in a "Spotify" playlist # Your Spotify playlists will by default be put in a "Spotify" playlist
# folder. If you would rather have them together with your other # folder. If you would rather have them together with your other
# playlists you can set this option to true. # 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 # Spotify playlists usually have many artist, and if you dont want
# every artist to be listed when artist browsing in Remote, you can set # every artist to be listed when artist browsing in Remote, you can set