Automatic Light Out

In my home I use YoLink switches for (smart) lights. While you can do a lot of control from their app, sooner or later you’ll end up with Home Assistant for automations.

In my case, I wanted a simple 2 minute timer every time my light switch gets pressed. Since each switch has additional 2 buttons, I wanted to use them to extend light-on state to 30 and 60 minutes. And most importantly, light automation should survive Home Assistant restart. Yes, one can probably just check lights manually after a restart, but I promise one won’t. So, it’s best to make restart survivable.

For lights on, rules are easy. YoLink switch is smart enough to turn on the list itself. So our only action is to start the timer.

triggers:
  - trigger: light.turned_on
    id: light_on
    target:
      entity_id: light.stairwell_light

actions:
  - choose:
      - conditions:
          - condition: trigger
            id: light_on
          - condition: state
            entity_id: timer.stairwell_light_off
            state: idle
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '00:02:00'

And what do we do when timer is reached? Well, we turn the light off.

triggers:
  - trigger: event
    id: timer_finished
    event_type: timer.finished
    event_data:
      entity_id: timer.stairwell_light_off

actions:
  - choose:
      - conditions:
          - condition: trigger
            id: timer_finished
        sequence:
          - action: light.turn_off
            target:
              entity_id: light.stairwell_light

These two cover 95% of what I needed. But we haven’t dealt with buttons yet. And with YoLink, that is a special task since they are not directly exposed. But they are exposed as events you can catch in Tools,Events menu, if you listen to yolink_event. With IDs in place, we can now adjust light-off timer to 30 or 60 minutes. In my case, due to 3-way switch setup, I had to capture 4 events.

triggers:
  - trigger: event
    id: button_1a
    event_type: yolink_event
    event_data:
      device_id: 8ff8e36d5d21125a07c9cb322f7d6be8
      type: button_1_short_press
  - trigger: event
    id: button_2a
    event_type: yolink_event
    event_data:
      device_id: 8ff8e36d5d21125a07c9cb322f7d6be8
      type: button_2_short_press
  - trigger: event
    id: button_1b
    event_type: yolink_event
    event_data:
      device_id: 87af18c6d107df22aaeb28b09f24b40d
      type: button_1_short_press
  - trigger: event
    id: button_1a
    event_type: yolink_event
    event_data:
      device_id: 87af18c6d107df22aaeb28b09f24b40d
      type: button_2_short_press

actions:
  - choose:
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: button_1a
              - condition: trigger
                id: button_1b
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '00:30:00'
          - action: light.turn_on
            target:
              entity_id: light.stairwell_light
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: button_2a
              - condition: trigger
                id: button_2b
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '01:00:00'
          - action: light.turn_on
            target:
              entity_id: light.stairwell_light

And now finally we’re onto the last condition - surviving HA restart. In theory, it should be as simple as just turning any light with expired timer off. But, in reality, that won’t work with YoLink switches because it takes a bit of time to establish communication with them. You also cannot check if light is on because light status is also a part that needs YoLink communication. So, the best you can do is to blindly turn on timer if one is not already on. If light is on, it will turn it off in 2 minutes. If light is off, we’ll turn it off again - not really an issue.

triggers:
  - trigger: homeassistant
    id: ha_start
    event: start

actions:
  - choose:
      - conditions:
          - condition: trigger
            id: ha_start
          - condition: timer.is_idle
            target:
              entity_id: timer.stairwell_light_off
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration:
                hours: 0
                minutes: 2
                seconds: 0

And only with all these is place we have our automation ready for the real life.

My full automation is below, adjust as needed.

alias: Stairwell Light
triggers:
  - trigger: homeassistant
    id: ha_start
    event: start
  - trigger: light.turned_on
    id: light_on
    target:
      entity_id: light.stairwell_light
  - trigger: light.turned_off
    id: light_off
    target:
      entity_id: light.stairwell_light
  - trigger: event
    id: button_1a
    event_type: yolink_event
    event_data:
      device_id: 8ff8e36d5d21125a07c9cb322f7d6be8
      type: button_1_short_press
  - trigger: event
    id: button_2a
    event_type: yolink_event
    event_data:
      device_id: 8ff8e36d5d21125a07c9cb322f7d6be8
      type: button_2_short_press
  - trigger: event
    id: button_1b
    event_type: yolink_event
    event_data:
      device_id: 87af18c6d107df22aaeb28b09f24b40d
      type: button_1_short_press
  - trigger: event
    id: button_1a
    event_type: yolink_event
    event_data:
      device_id: 87af18c6d107df22aaeb28b09f24b40d
      type: button_2_short_press
  - trigger: event
    id: timer_finished
    event_type: timer.finished
    event_data:
      entity_id: timer.stairwell_light_off
conditions: []
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: ha_start
          - condition: timer.is_idle
            target:
              entity_id: timer.stairwell_light_off
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration:
                hours: 0
                minutes: 2
                seconds: 0
      - conditions:
          - condition: trigger
            id: light_on
          - condition: state
            entity_id: timer.stairwell_light_off
            state: idle
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '00:02:00'
      - conditions:
          - condition: trigger
            id: light_off
        sequence:
          - action: timer.cancel
            target:
              entity_id: timer.stairwell_light_off
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: button_1a
              - condition: trigger
                id: button_1b
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '00:30:00'
          - action: light.turn_on
            target:
              entity_id: light.stairwell_light
      - conditions:
          - condition: or
            conditions:
              - condition: trigger
                id: button_2a
              - condition: trigger
                id: button_2b
        sequence:
          - action: timer.start
            target:
              entity_id: timer.stairwell_light_off
            data:
              duration: '01:00:00'
          - action: light.turn_on
            target:
              entity_id: light.stairwell_light
      - conditions:
          - condition: trigger
            id: timer_finished
        sequence:
          - action: light.turn_off
            target:
              entity_id: light.stairwell_light
mode: queued

Syncing YoLink Dimmers

Illustration

In my home automation I stumbled at an insteresting problem. All my switches are of YoLink’s smarty pants variety. One salient to this topic was their dimmer switch. I really wanted one of them to control the other and, while you can pair on/off state in YoLink, you cannot sync dim state. Home Assistant to the rescue!

My first go was simple enough

triggers:
  - trigger: state
    entity_id:
      - light.living_room_ceiling_light_switch

conditions: []

actions:
  - choose:
      - conditions:
          - condition: state
            entity_id: light.living_room_ceiling_light_switch
            state:
              - 'off'
        sequence:
          - action: light.turn_off
            target:
              entity_id: light.living_room_ceiling_light2_switch
            data: {}
      - conditions:
          - condition: state
            entity_id: light.living_room_ceiling_light_switch
            state:
              - 'on'
        sequence:
          - action: light.turn_on
            target:
              entity_id: light.living_room_ceiling_light2_switch
            data:
              brightness: >
                {{ state_attr('light.living_room_ceiling_light_switch',
                'brightness') | int }}

This will capture any change from my “controller” light and then propagate them to the “controllee”. If state is off, it will turn it off - simple enough. If state is on, it will turn it on and send the current dimmer state.

In theory, this should be enough. In practice, it worked ok if I adjusted dimmer values slowly. But, if I went fast, it would go into “Device is busy” state and never update. I did try “queued” and “restart” modes but neither really helped.

So, I turned to timers for help. Instead of reacting only to the changes, automation will also trigger every 5 seconds to check if states match. If states are the same, trigger is abandoned. If states are not the same, we proceed with the same change.

triggers:
  - trigger: state
    entity_id:
      - light.living_room_ceiling_light_switch
  - trigger: time_pattern
    seconds: /5

conditions:
  - condition: template
    value_template: |
      {{ states('light.living_room_ceiling_light2_switch') !=
        states('light.living_room_ceiling_light_switch')
        or
        (
          states('light.living_room_ceiling_light_switch') == 'on'
          and
          state_attr('light.living_room_ceiling_light2_switch', 'brightness') !=
          state_attr('light.living_room_ceiling_light_switch', 'brightness')
        ) }}

actions:
  - choose:
      - conditions:
          - condition: state
            entity_id: light.living_room_ceiling_light_switch
            state:
              - 'off'
        sequence:
          - action: light.turn_off
            target:
              entity_id: light.living_room_ceiling_light2_switch
            data: {}
      - conditions:
          - condition: state
            entity_id: light.living_room_ceiling_light_switch
            state:
              - 'on'
        sequence:
          - action: light.turn_on
            target:
              entity_id: light.living_room_ceiling_light2_switch
            data:
              brightness: >
                {{ state_attr('light.living_room_ceiling_light_switch',
                'brightness') | int }}

mode: restart

Now, this does two things. First, for slow updates, it will run only once and we’re golden. But, if there is an issue with the first run, it will run every 5 seconds until it manages to synchronize their states. Why 5 seconds? Well, sending updates more often actually prolongs “too busy” state to 20-30 seconds.

Also, I switched mode to restart. While default single did work ok, it also went through many intermediate brightness steps too making overall response slower.

Are there any places for improvement? Surely there are. One that immediately comes to mind is using proper 5 second timer instead of triggering every 5 seconds. I opted to skip this in order to have only a single automation. The performance difference was not worth the clutter.

Also, addint a small delay before choose conditions does help since “restart” mode gets to cancel it most of the time. Albeit that comes at a cost of slower single-trigger updates. Since most of my actions are just turning on or off the light, it seemed as unnecessary slowdown for a rare benefit.

Regardless, my second light now takes all its cues from my main light and that’s all I needed for now.

Adding battery charge to Tmux status bar

If you are not aware of tmux, do check it out. It’s a solution for problem you didn’t know you have. And, once you’re in its grasp, it’s very hard to get out.

One nice feature it does have is permanent status bar. My status bar used to just show CPU load.

set-window-option -g status-right ' #( vmstat 1 2 | tail -1 | awk "{ USAGE=100-\$15; if (USAGE<20) { printf \"#[fg=green]\"; } else if (USAGE<80) { printf \"#[fg=brightyellow]\"; } else { printf \"#[bg=red]#[fg=brightwhite]\"; }; print \" \" USAGE \"%\" }" ) '

However, after my laptop turned off on me while logged on remotely once too many, I decided to have battery charge there too. Since I don’t really care about the exact percentage, I decided to have it represented as a single “block” (▁▂▃▄▅▆▇█) character. To get capacity, I just used /sys/class/power_supply/BAT*/capacity. With percentage in hand, I then used a bit of math to have each block represent 14% of the charge, with the last 2% of charge using the last block. Since I use reverse coloring for status bar, blocks are ordered in increasing order.

#( awk '\''{print substr("▁▂▃▄▅▆▇█", int($1*7/98)+1, 1)}'\'' /sys/class/power_supply/BAT*/capacity || echo "█" )'

So, mission accomplished! Well, there’s always some scope creep. In my case, I also wanted to see charging status. This can be found in /sys/class/power_supply/BAT*/status. If value is “Charging”, I wanted negative space above battery block to be a different color.

#( awk '\''$1=="Charging" {print "#[fg=brightmagenta]"; next} {print "#[fg=black]"}'\'' /sys/class/power_supply/BAT*/status )#[reverse]#( awk '\''{print substr("▁▂▃▄▅▆▇█", int($1*7/98)+1, 1)}'\'' /sys/class/power_supply/BAT*/capacity || echo "█" )'

Illustration

Essentially, if status is “Charging”, awk will output bright magenta ANSI foreground code. This then gets reversed (i.e. background and foreground swapped), followed by battery charge block. Probably as much information as one can push into a single character.


PS: For curious, this is my full status line.

set-window-option -g status-right ' #( vmstat 1 2 | tail -1 | awk "{ USAGE=100-\$15; if (USAGE<20) { printf \"#[fg=green]\"; } else if (USAGE<80) { printf \"#[fg=brightyellow]\"; } else { printf \"#[bg=red]#[fg=brightwhite]\"; }; print \" \" USAGE \"%\" }" )#( awk '\''$1=="Charging" {print "#[fg=brightmagenta]"; next} {print "#[fg=black]"}'\'' /sys/class/power_supply/BAT*/status )#[reverse]#( awk '\''{print substr("▁▂▃▄▅▆▇█", int($1*7/98)+1, 1)}'\'' /sys/class/power_supply/BAT*/capacity || echo "█" )'

Goto in Spirit But Not in Name

I’ve heard statements like “good programmer should never use goto” a million times. Due to various reasons, some good and some bad, goto command is really hated. I would say it’s hated so much that quite a few young programmers are even not aware of its existence. One could say its existence in C# is treated as measels. And, just like measels, it’s making a comeback.

First of all, why would you use goto? For example, if we’re checking something deep in loops, there is no easier way to exit them fully than a humble goto. Consider the following code:

for (var i = 0; i < 100; i++) {
  for (var j = 0; j < 100; j++) {
    if (something) { goto Done; }
  }
}

Done:
// proceed with other code

Yes, you can get the same effect by using break at each level but this is definitely a nicer way to do it. And I would argue that this is NOT a misuse of goto. Regardless, C# 15 is preparing a death nail for this usage too.

New feature is called Labeled break and continue. In C# 15, my example above would now look like this:

Work:
for (var i = 0; i < 100; i++) {
  for (var j = 0; j < 100; j++) {
    if (something) { break Work; }
  }
}

// proceed with other code

I would argue this is a goto in disguise. Just a syntactic sugar that changes what gets labeled but doesn’t really change the flow.

That said, I am not really against it. I see how in many situations it will be more expressive than goto was. Maybe it’s time for goto to retire.

I for one welcome “Labeled break and continue” aka goto2. :)

MKV Video Bitrate

Getting bitrate used for video is easy using ffmpeg.

ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of csv=p=0 "$FILENAME"

That is, unless the file is .mkv where this command will return one big fat N/A. Due to various intricacies of Matroska container, you need to do a few more steps.

First step would be to get duration. And that goes as vel as expected.

ffprobe -v error -show_entries format=duration -of csv=p=0 "$FILENAME"

Then we need to get the size of the video stream. We cannot do this directly since our stream is “chopped” into many pieces. But with the help of awk we can sum all those chunks.

ffprobe -v error -select_streams v:0 -show_entries packet=size -of csv=p=0 "$FILENAME" | awk '{sum += $1} END {print sum+0}'

With those two we have all building blocks to determine video (and just video) bandwidth.

STREAM_DURATION=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$FILENAME" | cut -d. -f1)
STREAM_BYTES=$(ffprobe -v error -select_streams v:0 -show_entries packet=size -of csv=p=0 "$FILENAME" | awk '{sum += $1} END {print sum+0}')
BIT_RATE=$((STREAM_BYTES * 8 / STREAM_DURATION))
echo "$((BIT_RATE / 1000))K"