From b794b912a099b9e37325c0b6ebfc9f062d1f27f4 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 01:11:04 +0200 Subject: [PATCH 01/17] recent hass changes --- roles/hass/files/packages/amp.yaml | 117 +++++++++ roles/hass/files/packages/fans.yaml | 132 +++++++++++ roles/hass/files/packages/grow_lights.yaml | 223 ++++++++++++++++++ roles/hass/files/packages/media_center.yaml | 123 ++++++++++ .../hass/files/packages/usb_led_strings.yaml | 88 +++++++ roles/hass/tasks/hass.yml | 33 ++- roles/hass/templates/climate.yaml.j2 | 1 - roles/hass/templates/configuration.yaml.j2 | 89 ++++--- roles/hass/templates/git-hass-config.sh.j2 | 21 +- roles/hass/templates/hass-cron.j2 | 3 - .../templates/packages/toothbrush.yaml.j2 | 54 +++++ roles/hass/templates/templates.yaml.j2 | 47 +++- 12 files changed, 873 insertions(+), 58 deletions(-) create mode 100644 roles/hass/files/packages/amp.yaml create mode 100644 roles/hass/files/packages/fans.yaml create mode 100644 roles/hass/files/packages/grow_lights.yaml create mode 100644 roles/hass/files/packages/media_center.yaml create mode 100644 roles/hass/files/packages/usb_led_strings.yaml create mode 100644 roles/hass/templates/packages/toothbrush.yaml.j2 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..b88ace1 --- /dev/null +++ b/roles/hass/files/packages/grow_lights.yaml @@ -0,0 +1,223 @@ +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: state + entity_id: input_boolean.grow_lights_1_state + for: + hours: 0 + minutes: 5 + seconds: 0 + to: "on" + id: "on" + - platform: state + entity_id: input_boolean.grow_lights_1_state + for: + hours: 0 + minutes: 5 + seconds: 0 + to: "off" + id: "off" + condition: [] + action: + - service: >- + script.grow_lights_1_{{ trigger.id }} + 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/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" -- 2.40.1 From c51e924bca91c67f3abc6e50a3b5baeccc9958bb Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 12:10:37 +0200 Subject: [PATCH 02/17] owntone: separate spotify playlists --- roles/owntone/templates/owntone.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/owntone/templates/owntone.conf.j2 b/roles/owntone/templates/owntone.conf.j2 index 390157a..0f86466 100644 --- a/roles/owntone/templates/owntone.conf.j2 +++ b/roles/owntone/templates/owntone.conf.j2 @@ -379,7 +379,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 -- 2.40.1 From 9c8e635793c1831b085968dd4d1d43ec41eb4c80 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Tue, 2 May 2023 17:12:07 +0200 Subject: [PATCH 03/17] restic output handling in subprocess --- roles/backup/files/restic-backups.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/roles/backup/files/restic-backups.py b/roles/backup/files/restic-backups.py index 89b7b19..8fca745 100755 --- a/roles/backup/files/restic-backups.py +++ b/roles/backup/files/restic-backups.py @@ -89,7 +89,9 @@ def run_restic(repo_url, restic_args, dry_run, non_interactive): restic_cmd = ["restic", "-r", repo_url] restic_cmd.extend(restic_args) - if "backup" in restic_args: + backup = "backup" in restic_args + + if backup: restic_cmd.extend([ "--exclude-file", "/usr/local/etc/backup-excludes.txt" ]) @@ -105,7 +107,18 @@ 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=backup) + 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 + 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: -- 2.40.1 From db80ecb98ea96d18f48c3c03e6f95e84f18051cd Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 13:44:13 +0200 Subject: [PATCH 04/17] fixing date metadata from abs --- .../audiobookshelf/files/fix-podcast-date.py | 172 ++++++++++++++++++ roles/audiobookshelf/files/releasedate.sh | 168 +++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100755 roles/audiobookshelf/files/fix-podcast-date.py create mode 100755 roles/audiobookshelf/files/releasedate.sh diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py new file mode 100755 index 0000000..66e762b --- /dev/null +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import json +import sys +import shutil +import os + +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} not found in ffprobe stdout: {p.stdout}") + 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 ? + cmd = [ + "eyeD3", + "--release-date", iso_date_str, + podcast_file + ] + 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 parse_iso_date(date_str): + try: + dt = dateutil.parser.parse(date_str) + return dt.date().isoformat() + except (ParserError, TypeError) as e: + logger.error(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.warning(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") + + logger.debug(f"TDAT: '{tag_TDAT}', date: '{tag_date}'") + + parsed_TDAT = parse_TDAT_tag(tag_TDAT) + parsed_date = parse_iso_date(tag_date) + + if parsed_TDAT != parsed_date: + logger.warning(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!") + + logger.debug(f"date: {parsed_date}") + + if parsed_date is not None: + return parsed_date + elif parsed_TDAT is not None: + return parsed_TDAT + else: + logger.error(f"no valid date found in '{file_path}'") + raise SystemExit(3) + + +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") + 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.info(os.path.basename(args.podcast_file)) + + date = get_iso_date_in_file(args.podcast_file) + + if args.ffmpeg: + ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) + else: + eyeD3_write_date_tag(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 -- 2.40.1 From 0a75be631c7824bb200e64f5468fa706f90ccf77 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 14:14:13 +0200 Subject: [PATCH 05/17] error msg --- roles/audiobookshelf/files/fix-podcast-date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 66e762b..6da8c1d 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -47,7 +47,7 @@ def ffprobe_get_tags(file_path): 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} not found in ffprobe stdout: {p.stdout}") + logger.error(f"key {e} for file '{file_path}' not found in ffprobe stdout: {p.stdout}") raise SystemExit(2) -- 2.40.1 From 4d219186109925b0a5619807a85c58405aca1a9d Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 14:22:13 +0200 Subject: [PATCH 06/17] error handling --- .../audiobookshelf/files/fix-podcast-date.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 6da8c1d..777d776 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +from datetime import datetime import subprocess import json import sys @@ -115,23 +116,34 @@ def get_iso_date_in_file(file_path): tag_TDAT = tags.get("TDAT") tag_date = tags.get("date") - logger.debug(f"TDAT: '{tag_TDAT}', date: '{tag_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.info(f"date: {parsed_date}") + if parsed_TDAT != parsed_date: logger.warning(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!") - logger.debug(f"date: {parsed_date}") - if parsed_date is not None: return parsed_date - elif parsed_TDAT is not None: - return parsed_TDAT else: - logger.error(f"no valid date found in '{file_path}'") - raise SystemExit(3) + return parsed_TDAT + + +def date_tag_is_ok(file_path): + tags = ffprobe_get_tags(file_path) + tag_date = tags.get("date") + try: + datetime.fromisoformat(tag_date) + return True + except ValueError: + return False def parse_args(): @@ -160,12 +172,15 @@ def main(): date = get_iso_date_in_file(args.podcast_file) - if args.ffmpeg: - ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) + if date_tag_is_ok(args.podcast_file): + logger.info(f"date tag is ok in file '{args.podcast_file}', did not touch file.") else: - eyeD3_write_date_tag(args.podcast_file, date) + if args.ffmpeg: + ffmpeg_write_date_tag(args.podcast_file, args.out_file, date) + else: + eyeD3_write_date_tag(args.podcast_file, date) - logger.success(f"updated date in '{args.podcast_file}' as {date}") + logger.success(f"updated date in '{args.podcast_file}' as {date}") if __name__ == "__main__": -- 2.40.1 From 5cea25f4ef5dbaa6282334eda347b2ce216791a0 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 14:22:13 +0200 Subject: [PATCH 07/17] decode --- roles/audiobookshelf/files/fix-podcast-date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 777d776..69f3f2d 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -48,7 +48,7 @@ def ffprobe_get_tags(file_path): 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}") + logger.error(f"key {e} for file '{file_path}' not found in ffprobe stdout: {p.stdout.decode()}") raise SystemExit(2) -- 2.40.1 From 9a1e71a79b3d2766a6b2b27c7848f36270f3401b Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 14:22:13 +0200 Subject: [PATCH 08/17] log levels --- roles/audiobookshelf/files/fix-podcast-date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 69f3f2d..721f43d 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -106,7 +106,7 @@ def parse_TDAT_tag(tag_tdat): iso_date_str = tag_tdat.split(' ')[0] return parse_iso_date(iso_date_str) except (AttributeError, IndexError) as e: - logger.warning(f"invalid 'TDAT' tag: '{tag_tdat}'") + logger.debug(f"invalid 'TDAT' tag: '{tag_tdat}'") return None @@ -128,7 +128,7 @@ def get_iso_date_in_file(file_path): logger.info(f"date: {parsed_date}") if parsed_TDAT != parsed_date: - logger.warning(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!") + logger.debug(f"dates in 'TDAT' ({parsed_TDAT}) and 'date' ({parsed_date}) differ!") if parsed_date is not None: return parsed_date -- 2.40.1 From 446ed550974169b14abdb9c51351776300cdcd54 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 15:18:13 +0200 Subject: [PATCH 09/17] file utime --- .../audiobookshelf/files/fix-podcast-date.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 721f43d..7a91ab8 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -7,6 +7,7 @@ import json import sys import shutil import os +import time from loguru import logger import dateutil.parser @@ -136,15 +137,21 @@ def get_iso_date_in_file(file_path): return parsed_TDAT -def date_tag_is_ok(file_path): +def file_dates_are_ok(file_path): tags = ffprobe_get_tags(file_path) tag_date = tags.get("date") try: - datetime.fromisoformat(tag_date) - return True + 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()) + os.utime(file_path, (ts, ts)) + def parse_args(): parser = argparse.ArgumentParser() @@ -172,16 +179,16 @@ def main(): date = get_iso_date_in_file(args.podcast_file) - if date_tag_is_ok(args.podcast_file): - logger.info(f"date tag is ok in file '{args.podcast_file}', did not touch file.") + if file_dates_are_ok(args.podcast_file): + logger.info(f"metadata date and filesystem utimes are ij fir {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() -- 2.40.1 From 96781afe3669b419d29a9a3250c82c87dc023585 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 15:18:13 +0200 Subject: [PATCH 10/17] log --- roles/audiobookshelf/files/fix-podcast-date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 7a91ab8..b5d1496 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -99,7 +99,7 @@ def parse_iso_date(date_str): dt = dateutil.parser.parse(date_str) return dt.date().isoformat() except (ParserError, TypeError) as e: - logger.error(f"invalid date string: '{date_str}'") + logger.warning(f"invalid date string: '{date_str}'") def parse_TDAT_tag(tag_tdat): -- 2.40.1 From 4f55fef2e72911d808cd6d0492536f23889461e1 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 15:20:13 +0200 Subject: [PATCH 11/17] cleanup --- .../audiobookshelf/files/fix-podcast-date.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index b5d1496..72f6740 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -126,7 +126,7 @@ def get_iso_date_in_file(file_path): else: logger.debug(f"TDAT: '{parsed_TDAT}' ('{tag_TDAT}'), date: '{parsed_date}' ('{tag_date}')") - logger.info(f"date: {parsed_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!") @@ -150,7 +150,11 @@ def file_dates_are_ok(file_path): 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)) + os.utime(os.path.dirname(file_path), (ts, ts)) + return dt def parse_args(): @@ -160,6 +164,7 @@ def parse_args(): 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") args = parser.parse_args() if not args.debug: @@ -175,12 +180,16 @@ def parse_args(): def main(): args = parse_args() - logger.info(os.path.basename(args.podcast_file)) + logger.debug(f"checking: '{os.path.basename(args.podcast_file)}'") date = get_iso_date_in_file(args.podcast_file) - if file_dates_are_ok(args.podcast_file): - logger.info(f"metadata date and filesystem utimes are ij fir {args.podcast_file}', did not modify file") + if args.mtime: + dt = set_utime(args.podcast_file, 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) @@ -190,5 +199,7 @@ def main(): set_utime(args.podcast_file, date) logger.success(f"updated date in '{args.podcast_file}' as {date}") + + if __name__ == "__main__": main() -- 2.40.1 From 995d88f0f74cb5a2128826acb2b6c92af5b52f60 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Mon, 1 May 2023 17:02:13 +0200 Subject: [PATCH 12/17] task --- .../audiobookshelf/files/fix-podcast-date.py | 21 ++++++++++++++++--- roles/audiobookshelf/tasks/audiobookshelf.yml | 16 ++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 72f6740..451c43b 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -63,7 +63,7 @@ def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str): "-i", podcast_file, #"-c", "copy", "-metadata", f"date={iso_date_str}", - # "-metadata", f"releasedate={iso_date_str}", + #"-metadata", f"releasedate={iso_date_str}", out_file ] @@ -84,8 +84,20 @@ def eyeD3_write_date_tag(podcast_file, iso_date_str): cmd = [ "eyeD3", "--release-date", iso_date_str, - podcast_file + "--recording-date", iso_date_str, + "--orig-release-date", iso_date_str, + "--release-year", iso_date_str.split("-")[0], + "--preserve-file-times" ] + podcast_dir = os.path.dirname(podcast_file) + cover_path = os.path.join(podcast_dir, "cover.jpg") + + if os.path.exists(cover_path) and False: + cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"]) + cmd.append(podcast_file) + + logger.info(cmd) + try: subprocess.run(cmd, capture_output=True, check=True, stdin=None) logger.debug(f"updated: '{podcast_file}'") @@ -153,7 +165,10 @@ def set_utime(file_path, iso_date_str): # shutil.move(file_path, f"{file_path}.new") # shutil.move(f"{file_path}.new", file_path) os.utime(file_path, (ts, ts)) - os.utime(os.path.dirname(file_path), (ts, ts)) + try: + os.utime(os.path.dirname(file_path), (ts, ts)) + except FileNotFoundError: + pass return dt 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 -- 2.40.1 From ddc099fbfaa316e3643b3779c00e847adca42436 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Thu, 4 May 2023 01:42:13 +0200 Subject: [PATCH 13/17] airconnect codec inventory var --- roles/airconnect/defaults/main.yml | 2 ++ roles/airconnect/templates/airupnp.xml.j2 | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 }} -- 2.40.1 From a54860da04a94c4ae435270f78aff166813d4b3f Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Thu, 4 May 2023 01:42:21 +0200 Subject: [PATCH 14/17] --year overwrites dates --- .../audiobookshelf/files/fix-podcast-date.py | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/roles/audiobookshelf/files/fix-podcast-date.py b/roles/audiobookshelf/files/fix-podcast-date.py index 451c43b..b5ac854 100755 --- a/roles/audiobookshelf/files/fix-podcast-date.py +++ b/roles/audiobookshelf/files/fix-podcast-date.py @@ -80,23 +80,26 @@ def ffmpeg_write_date_tag(podcast_file, out_file, iso_date_str): 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, - "--recording-date", iso_date_str, "--orig-release-date", iso_date_str, - "--release-year", iso_date_str.split("-")[0], - "--preserve-file-times" + "--recording-date", iso_date_str, + # this overwrites 'release date' i think: + #"--release-year", iso_date_str.split("-")[0], + #"--preserve-file-times" ] - podcast_dir = os.path.dirname(podcast_file) - cover_path = os.path.join(podcast_dir, "cover.jpg") - - if os.path.exists(cover_path) and False: - cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"]) + # if os.path.exists(cover_path): + # cmd.extend(["--add-image", f"{cover_path}:FRONT_COVER"]) cmd.append(podcast_file) - logger.info(cmd) + logger.debug(" ".join(cmd)) try: subprocess.run(cmd, capture_output=True, check=True, stdin=None) @@ -106,6 +109,28 @@ def eyeD3_write_date_tag(podcast_file, iso_date_str): 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) @@ -180,6 +205,8 @@ def parse_args(): 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: @@ -197,10 +224,17 @@ 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_file, date) + 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): -- 2.40.1 From 528a430a6dc73e1a189389187bc13f5a36b70f23 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Thu, 4 May 2023 01:42:33 +0200 Subject: [PATCH 15/17] cleanup --- roles/backup/files/restic-backups.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/roles/backup/files/restic-backups.py b/roles/backup/files/restic-backups.py index 8fca745..6313aff 100755 --- a/roles/backup/files/restic-backups.py +++ b/roles/backup/files/restic-backups.py @@ -89,9 +89,7 @@ def run_restic(repo_url, restic_args, dry_run, non_interactive): restic_cmd = ["restic", "-r", repo_url] restic_cmd.extend(restic_args) - backup = "backup" in restic_args - - if backup: + if "backup" in restic_args: restic_cmd.extend([ "--exclude-file", "/usr/local/etc/backup-excludes.txt" ]) @@ -108,15 +106,16 @@ def run_restic(repo_url, restic_args, dry_run, non_interactive): # .run(env={}) is possible (but then child doesnt inherit parent env) try: - ps = subprocess.run(restic_cmd, check=True, capture_output=backup) - if ps.stdout is not None: - summary = ps.stdout.decode().split('\n')[-7:] - if backup: - logger.success(f"{summary[2].strip()} ({summary[4]})") + 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 - logger.error(e.stderr.decode()) + 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) -- 2.40.1 From 504e4440a95ace82dd6f2a6d4e8ebf4f8d173e4e Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Thu, 4 May 2023 01:42:46 +0200 Subject: [PATCH 16/17] pulseaudio --- roles/owntone/templates/owntone.conf.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/owntone/templates/owntone.conf.j2 b/roles/owntone/templates/owntone.conf.j2 index 0f86466..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" -- 2.40.1 From 6e0460cd0813b1c700647155d78e83fdcbef7434 Mon Sep 17 00:00:00 2001 From: Ben Kristinsson Date: Thu, 4 May 2023 01:43:05 +0200 Subject: [PATCH 17/17] disable automation not needed anyomore --- roles/hass/files/packages/grow_lights.yaml | 20 +++---------------- .../automations-ansible-managed.yaml.j2 | 2 ++ 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/roles/hass/files/packages/grow_lights.yaml b/roles/hass/files/packages/grow_lights.yaml index b88ace1..23a14bc 100644 --- a/roles/hass/files/packages/grow_lights.yaml +++ b/roles/hass/files/packages/grow_lights.yaml @@ -126,26 +126,12 @@ script: automation: - alias: grow_lights_1_state trigger: - - platform: state - entity_id: input_boolean.grow_lights_1_state - for: - hours: 0 - minutes: 5 - seconds: 0 - to: "on" - id: "on" - - platform: state - entity_id: input_boolean.grow_lights_1_state - for: - hours: 0 - minutes: 5 - seconds: 0 - to: "off" - id: "off" + - platform: time_pattern + minutes: /15 condition: [] action: - service: >- - script.grow_lights_1_{{ trigger.id }} + script.grow_lights_1_{{ states('binary_sensor.grow_lights_1_schedule') }} data: {} - alias: grow_lights_1_hass_start 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] %} -- 2.40.1