nocin.eu

Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[Home Assistant] Display Reolink Recordings from a NAS using the Gallery Card

A few months ago, I installed a Reolink Doorbell at our front door. Since then, I’ve used it for simple automations like sending a photo when someone is at the door. To capture a photo, I was using the camera.snapshot service. I never used the photos and videos that the doorbell itself recorded and that were stored on my TrueNAS system via FTP. Mainly because I haven’t found a good way to display the captured photos and videos on my Dashboard. But finally I was able to fill the gap with the Gallery Card, which turned out to be exactly what I needed the whole time. By using it, you can simply display the latest images and videos, even when they are stored in some kind of nested folder structure, and it also helps to parse filenames to display them in a more convenient way.

Following a brief overview of what I had to do:

  • TrueNAS
    • Create Dateset, in my case it’s: data/camera
    • Share the dataset via NFS
      • Sharing → Unix Shares (NFS) → Add → Choose your new dataset
    • Create some folder(s) on your new dataset, for example: /Reolink/Wifi-Doorbell/Recordings (to do that, simply mount the NFS share to your local machine or use the terminal)
    • Activate the FTP Service
      • Services → FTP
  • Reolink
    • Go to Settings → Surveillance → FTP
      • Insert your TrueNAS FTP credentials
      • Remote Directory: /Reolink/Wifi-Doorbell/Recordings
      • Generate subfolder by: YYYY-MM-DD
      • Select what and when you want to record something
  • Home Assistant
    • Add your Doorbell via Reolink Integration
    • Mount NFS Share
      • Settings → System → Storage → Add Network Storage
      • Name: Reolink (whatever you like)
      • Usage: Media
      • Path: /mnt/data/camera
    • Check if you can access your recordings
      • Media → My media → media → Reolink (NFS name)→ Reolink (my folder name)→ Wifi-Doorbell → Recordings
    • If you cannot open your files, you probably have to adjust your permission on the TrueNAS Dataset. Provide at least read permissions (e.g. 655)
      • Storage → Pools → camera → Edit Permissions
    • Install Gallery Card
      • HACS → Frontend → Explore & Download Repositories → Search for Gallery Card
      • Go to your Dashboard and add the card
type: custom:gallery-card
entities:
  - media-source://media_source/media/Reolink/Reolink/Wifi-Doorbell/Recordings/
maximum_files: 25
menu_alignment: top
folder_format: YYYY/MM/DD
video_loop: true
video_autoplay: true
video_muted: true
file_name_date_begins: 12
file_name_format: YYYYMMDDHHmmss
caption_format: HH:mm:ss DD/MM

Most variables are self-explanatory and also well explained by the galery-card docs, but below a few words about the settings I used, beginning with the path to the media which is provided on the entities attribute.

entities:
  - media-source://media_source/media/Reolink/Reolink/Wifi-Doorbell/Recordings/

The first part is a default value: media-source://media_source/
Followed by the usage type and name we have chosen when mounting the NSF share: media/Reolink/
Then we need the directory path: Reolink/Wifi-Doorbell/Recordings

Because we chose in the Reolink settings “Generate subfolder by: YYYY-MM-DD“, the files are stored in a hierarchy like this:

  • 2023
    • 10
    • 11
    • 12
      • 01
      • 02
      • 03
      • 04
      • 05

The gallery-card can automatically parse this, if you provide the following setting: folder_format: YYYY/MM/DD.
And because the filenames look like this: Haustuer_00_20231230143626.jpg, you have to add file_name_date_begins: 12 to skip the firs letters, and file_name_format: YYYYMMDDHHmmss to parse the date. With caption_format: HH:mm:ss DD/MM you can define the output, how you want to display the parsed timestamp.

[Android] Upgrading LineageOS 19.1 to 20.0 on my Xiaomi Mi 8 (dipper)

Install the Android Debug Bridge (ADB)

https://wiki.lineageos.org/adb_fastboot_guide.html
https://github.com/M0Rf30/android-udev-rules#installation

# check if device is found
adb devices
# reboot into sideload modus
adb reboot sideload

Or manually boot into TWRP recovery, holding Volume Up + Power when the phone is off. Navigate to AdvancedADB Sideload.


Update MIUI Firmware

Following the docs, I first had to check the Firmware version. V12.0.3.0.QEAMIXM was required, and I already had it installed.
If you’re on an older version, download the right MIUI Firmware for your device from https://xiaomifirmwareupdater.com/firmware/dipper/.
Flash the new Firmware via TWRP or via ADB sideload.

adb sideload fw_dipper_miui_MI8Global_V12.0.3.0.QEAMIXM_7619340f8c_10.0.zip

Download and flash new LineageOS image

I’m using the LineageOS fork LineageOS for microG. Download it from here: https://download.lineage.microg.org/dipper/ (MI 8 = dipper)
The upgrade steps are the same as for the official rom: https://wiki.lineageos.org/devices/dipper/upgrade. In my case only flashing the new image.

adb sideload lineage-20.0-20231204-microG-dipper.zip

[Home Assistant] Person card

I recently saw this cool dashboard and the person cards caught my eye. As the dashboard creator also provided all the yaml code and a really helpful guide, I tried to rebuild it. I was already using the mushroom-person-card, so I “just” had to add the little icons (mushroom-chips-card) below it (using the vertical-stack-in-card). To display the network type and the charging status, you first have to enable these sensors in the Home Assistant Companion App on your mobile phone. This is my result:

type: horizontal-stack
cards:
  - type: custom:vertical-stack-in-card
    cards:
      - type: custom:mushroom-person-card
        entity: person.nico
        icon_type: entity-picture
        secondary_info: last-changed
        card_mod:
          style: |
            ha-card {
              background: transparent;
            }
      - type: horizontal-stack
        cards:
          - type: custom:mushroom-chips-card
            alignment: center
            card_mod: 
              style: |
                ha-card {
                    --chip-font-size: 0.3em;
                    --chip-icon-size: 0.5em;
                    --chip-border-width: 0;
                    --chip-box-shadow: none;
                    --chip-background: none;
                    --chip-border: none;
                    --chip-spacing: none;
                    --chip-font-weight: bold;
                }
            chips:
              - type: template
                entity: sensor.mi_8_network_type
                content: '{{ states(''sensor.mi_8_network_type'')}}'                
                icon: |-
                  {% if is_state('sensor.mi_8_network_type','wifi')%}
                    mdi:wifi 
                  {% elif is_state('sensor.mi_8_network_type','vpn')%}
                    mdi:network
                  {% elif is_state('sensor.mi_8_network_type','cellular')%}
                    mdi:signal-4g
                  {% else %} 
                    mdi:network-strength-off
                  {% endif %}  
                icon_color: >-
                  {% if is_state('sensor.mi_8_network_type','wifi') or
                  is_state('sensor.mi_8_network_type','vpn')%}
                    green
                  {% elif is_state('sensor.mi_8_network_type','cellular')%}
                    red
                  {% else %} 
                    grey
                  {% endif %}   
                tap_action:
                  action: more-info
              - type: template
                entity: sensor.mi_8_battery_level
                content: '{{ states(''sensor.mi_8_battery_level'')}}%'
                icon: |-
                  {% set state = states('sensor.mi_8_battery_level')|float %}
                  {% if state >= 0 and state < 10 %} mdi:battery-10
                  {% elif state >= 10 and state < 20 %} mdi:battery-20
                  {% elif state >= 20 and state < 30 %} mdi:battery-30
                  {% elif state >= 30 and state < 40 %} mdi:battery-40
                  {% elif state >= 40 and state < 50 %} mdi:battery-50
                  {% elif state >= 50 and state < 60 %} mdi:battery-60
                  {% elif state >= 60 and state < 70 %} mdi:battery-70
                  {% elif state >= 70 and state < 80 %} mdi:battery-80
                  {% elif state >= 80 and state < 95 %} mdi:battery-90
                  {% else %} mdi:battery
                  {% endif %}   
                icon_color: |-
                  {% set state = states('sensor.mi_8_battery_level')|float %}
                  {% if state >= 0 and state < 20 %} red
                  {% elif state >= 20 and state < 50 %} orange
                  {% elif state >= 50 and state < 80 %} yellow
                  {% else %} green
                  {% endif %}         
                tap_action:
                  action: more-info
              - type: template
                entity: sensor.mi_8_battery_power
                content: '{{ states(''sensor.mi_8_battery_power'')}} W'
                icon: |-
                  {% if is_state('binary_sensor.mi_8_is_charging','on')%} 
                    mdi:power-plug  
                  {% else %} 
                    mdi:power-plug-off 
                  {% endif %}                  
                icon_color: |-
                  {% if is_state('binary_sensor.mi_8_is_charging','on')%} 
                    blue
                  {% else %} 
                    grey
                  {% endif %}
                tap_action:
                  action: more-info
  - type: custom:vertical-stack-in-card
    cards:
      - type: custom:mushroom-person-card
        entity: person.user2
        icon_type: entity-picture
        secondary_info: last-changed
        card_mod:
          style: |
            ha-card {
              background: transparent;
            }
      - type: horizontal-stack
        cards:
          - type: custom:mushroom-chips-card
            alignment: center
            card_mod: 
              style: |
                ha-card {
                    --chip-font-size: 0.3em;
                    --chip-icon-size: 0.5em;
                    --chip-border-width: 0;
                    --chip-box-shadow: none;
                    --chip-background: none;
                    --chip-border: none;
                    --chip-spacing: none;
                    --chip-font-weight: bold;
                }
            chips:
              - type: template
                entity: sensor.redmi_note_8_pro_network_type
                content: '{{ states(''sensor.redmi_note_8_pro_network_type'')}}'                
                icon: >-
                  {% if
                  is_state('sensor.redmi_note_8_pro_network_type','wifi')%}
                    mdi:wifi 
                  {% elif
                  is_state('sensor.redmi_note_8_pro_network_type','vpn')%}
                    mdi:network
                  {% elif
                  is_state('sensor.redmi_note_8_pro_network_type','cellular')%}
                    mdi:signal-4g
                  {% else %} 
                    mdi:network-strength-off
                  {% endif %}  
                icon_color: >-
                  {% if is_state('sensor.redmi_note_8_pro_network_type','wifi')
                  or is_state('sensor.redmi_note_8_pro_network_type','vpn')%}
                    green
                  {% elif
                  is_state('sensor.redmi_note_8_pro_network_type','cellular')%}
                    red
                  {% else %} 
                    grey
                  {% endif %}   
                tap_action:
                  action: more-info
              - type: template
                entity: sensor.mi_8_battery_level
                content: '{{ states(''sensor.redmi_note_8_pro_battery_level'')}}%'
                icon: >-
                  {% set state = states('sensor.redmi_note_8_pro_battery_level')|float %}
                  {% if state >= 0 and state < 10 %} mdi:battery-10
                  {% elif state >= 10 and state < 20 %} mdi:battery-20
                  {% elif state >= 20 and state < 30 %} mdi:battery-30
                  {% elif state >= 30 and state < 40 %} mdi:battery-40
                  {% elif state >= 40 and state < 50 %} mdi:battery-50
                  {% elif state >= 50 and state < 60 %} mdi:battery-60
                  {% elif state >= 60 and state < 70 %} mdi:battery-70
                  {% elif state >= 70 and state < 80 %} mdi:battery-80
                  {% elif state >= 80 and state < 95 %} mdi:battery-90
                  {% else %} mdi:battery
                  {% endif %}   
                icon_color: >-
                  {% set state = states('sensor.redmi_note_8_pro_battery_level')|float %}
                  {% if state >= 0 and state < 20 %} red
                  {% elif state >= 20 and state < 50 %} orange
                  {% elif state >= 50 and state < 80 %} yellow
                  {% else %} green
                  {% endif %}            
                tap_action:
                  action: more-info
              - type: template
                entity: sensor.redmi_note_8_pro_battery_power
                content: '{{ states(''sensor.redmi_note_8_pro_battery_power'')}} W'
                icon: >-
                  {% if is_state('binary_sensor.redmi_note_8_pro_is_charging','on')%} 
                    mdi:power-plug  
                  {% else %} 
                    mdi:power-plug-off 
                  {% endif %}                  
                icon_color: >-
                  {% if is_state('binary_sensor.redmi_note_8_pro_is_charging','on')%} 
                    blue
                  {% else %} 
                    grey
                  {% endif %}
                tap_action:
                  action: more-info

[Home Assistant] Template sensor which combines door lock and contact sensor

Cool template sensor idea I found here, which combines the states of two sensors and display the different combined states. Useful if, for example, you want to combine a door lock and a contact sensor on the same door.

Settings -> Devices & services -> Helpers -> Create Helper -> Template

{% if is_state('lock.kellertur', 'locked') and is_state('binary_sensor.kellertur_contact', 'off') %}
  Locked
{% elif is_state('lock.kellertur', 'unlocked') and is_state('binary_sensor.kellertur_contact', 'on') %}
  Open
{% elif is_state('lock.kellertur', 'unlocked') and is_state('binary_sensor.kellertur_contact', 'off') %}
  Closed, Unlocked
{% elif is_state('lock.kellertur', 'locked') and is_state('binary_sensor.kellertur_contact', 'on') %}
  Open, Locked 
{% else %}
  Unknown
{% endif %}

Helpful to check whether a door is really closed when locking it via a smart lock. Because else it could happen, that you would see status “Locked“, although the door is still open. But with this helper, you will get in this situation “Open, Locked“.

To emphasize such a situation even more clearly, you can of course also add some icon colors.

[CAP] Select max with group by

https://www.w3resource.com/sql/aggregate-functions/Max-with-group-by.php

https://answers.sap.com/questions/13081527/simple-count-aggregation-on-caps-cql.html?childToView=13079907&answerPublished=true

entity myEntity {
        key ID: Integer;
        key seqNr: Integer;
        key createdAt: DateTime;
        ...
    }

@readonly
entity myEntityMaxSeqNr 
        as select from myEntity {
            ID, 
            count(seqNr) as maxSeqNr: Integer
            createdAt,
            ...
            } 
        group by 
            ID,
            createdAt;

This can also be done using CQL like here.

[Home Assistant] Motion sensor in combination with Adaptive Lightning

I have some lights in my garden which are turned on in the night and are controlled by the Adaptive Lighting component, to automatically adjust brightness and color during the night. But if someone comes home late and this is detected by a motion sensor, I wanted to increase the brightness of all the lights in the garden for a short time.

Increasing the brightness was easy, as it can be done by using the light.turn_on service. However, it took me a few minutes to figure out how to reactivate adaptive lighting on these lights when motion is no longer detected. But it’s actually super simple (and it’s directly written on the GitHub start page here and here). You just have to deactivate the “manually controlled” flag that got activated by “manually” increasing the brightness. Following an example with a single motion sensor (binary_sensor.haustuer_motion), a lamp (light.haustur_light) and the adaptive lightning switch entity (switch.adaptive_lighting_haustuer).

alias: Motion sensor front door
description: "Increase brightness for three minutes when motion is detected"
trigger:
  - platform: state
    entity_id:
      - binary_sensor.haustuer_motion
    to: "on"
condition:
  - condition: state
    entity_id: sun.sun
    state: below_horizon
action:
  - service: light.turn_on
    data:
      transition: 3
      brightness_pct: 70
    target:
      entity_id: light.haustur_light
  - wait_for_trigger:
      - platform: state
        entity_id:
          - binary_sensor.haustuer_motion
        to: "off"
        for:
          hours: 0
          minutes: 3
          seconds: 0
    timeout:
      hours: 0
      minutes: 0
      seconds: 0
      milliseconds: 0
  - service: adaptive_lighting.set_manual_control
    data:
      manual_control: false
      entity_id: switch.adaptive_lighting_haustuer
      lights:
        - light.haustur_light
mode: single

[SAPUI5] Create date object in UTC YYYY-MM-ddTHH:mm:ss

https://sapui5.hana.ondemand.com/#/api/sap.ui.core.format.DateFormat%23methods/format

            const today = new Date()
            const oDateTimeFormat = sap.ui.core.format.DateFormat.getDateTimeInstance({
                pattern: "yyyy-MM-ddTHH:mm:ss",
                UTC: true
            })
            const todayISO = oDateTimeFormat.format(today)

The UTC flag can also be set, when calling the format function.

            const today = new Date()
            const oDateTimeFormat = sap.ui.core.format.DateFormat.getDateTimeInstance({
                pattern: "yyyy-MM-ddTHH:mm:ss",
                //UTC: true
            })
            const todayISO = oDateTimeFormat.format(today, true)

[SAPUI5] Adding Lanes to a Process Flow

Manually adding Lanes to a Process Flow Control:

https://sapui5.hana.ondemand.com/#/api/sap.suite.ui.commons.ProcessFlow
https://sapui5.hana.ondemand.com/#/api/sap.suite.ui.commons.ProcessFlowLaneHeader
https://sapui5.hana.ondemand.com/#/entity/sap.suite.ui.commons.ProcessFlow/sample/sap.suite.ui.commons.sample.ProcessFlowUpdateLanes/code

In my case, there was no way to bind the model to the view, so I did the mapping for each ProcessFlowLaneHeader in the callback function after reading the oData entity.

view.xml

<flow:ProcessFlow id="process-flow"/>

controller.js

var oProcessFlow = this.getView().byId("process-flow")

var oRequestFilter = new sap.ui.model.Filter({
    path: "myId",
    operator: sap.ui.model.FilterOperator.EQ,
    value1: myId
})

this.getView().getModel().read("/WorkflowSet", {
    filters: [oFormularIdFilter],
    success: (oData, response) => {
        for (var i = 0; i < oData.results.length; i++) {
            var oLaneHeader = new ProcessFlowLaneHeader({
                laneId: oData.results[i].LaneId,
                iconSrc: oData.results[i].IconSrc,
                text: oData.results[i].Text,
                position: oData.results[i].Position,
                state: [{state: oData.results[i].State, value: "100"}]
            });
            oProcessFlow.addLane(oLaneHeader)
        }
    },
    error: oError => {
        sap.m.MessageToast.show("An error occured while reading entity /WorkflowSet.")
    }
});