Skip to content

Fixing a Broken Home Assistant

Like a small child with no sense of fear, my Home Assistant continues to break itself...

In my last post, I had a bit of a rant about how difficult my smart home was being. Since then, I've had a bit of time to recollect my thoughts and started to systematically tackle the issues and pain points I've had. In general, the issues in vague order of how much they annoy me are:

  • Unreliable Smart Plug Connections
    • Reliance on 'The Cloud'
    • Lack of Documentation
    • Alexa Integration
  • Remote Access
    • Access via http://host.name:8123
    • Lack of TLS
    • Lack of External Access
  • Scenes!
    • Automation
    • Sync Computer Brightness/Hue
  • Future Changes
    • Energy and Cost Views
      • Personal Usage
      • Total House Usage
    • Network Isolation
    • Camera (inc. Doorbell) Access
    • Expand to Entire House
    • Get Remote Keybinds on Computer Working Again
    • Auto Off/On Arrive/Leave House
    • Scene Switch Offs Only/Override Options
    • Electric Blanket Switch Off
    • Plex Dim Lights

I won't tackle all of those in this post, I'll just chip away at those I currently find most annoying and easiest to fix.

Unreliable Smart Plug Connections

Historically, smart plugs have randomly become unavailable and cannot be controlled directly through Home Assistant. In part, I think it's due to some dodgy firmware on my Netgear WAX214, and because some plugs go via the cloud.

To fix the AP issue, I really ought to start aggregating long term data and run multiple APs. I should really also try moving to Zigbee or some other 802.15.4-based standard, as these are designed for constrained devices with low power and processing overheads, with built in long-distance and noise-resistance built in.

Cloud

I now buy Local Bytes plugs (although these have their own issues), and am increasingly trying to remove cloud dependencies in the remaining plugs, either entirely reflashing the firmware (after all, there has to be some way to program a firmware), or using things such as Tuya Cloudcutter and Local Tuya for a more lightweight, over the air patch.

Lack of Documentation

Another issue I have had historically is the lack of documentation. If I can't ping a plug (ideally via DNS, although as a last resort IP), then something is amiss and that should probably be where I look first.

I document everything under my home lab tab on the site, specifically here for my IoT devices. I've gone through and updated the documentation here, so I can now see what devices are where on the network, the firmware they are running and the locations they physically are in the house.

Now I can see all this, I know what is broken, where it is, and hopefully how much of a pain it will be to fix. The other upside of proper documentation is that I can now assign the plugs by local-based first, then move onto or patch a cloud-based plug as and when I need to use it.

Maintaining Compatibility with Alexa

Whilst I am actively looking for alternatives to the Amazon Echo lineup of products, I still use them as they have simple music provider integration and can be used to turn lights on and off. I 100% will get rid of them the moment I hear even one advert or have to start paying a subscription, as I think I could hack together a mostly local replacement.

For now, Alexa thinks that the devices I expose are all light bulbs, as I'm using the Emulated Hue integration. This hasn't worked flawlessly, and I find myself needing to delete and re-add devices commonly. Another issue I currently have is that Alexa uses SSDP broadcasts to try and find the emulated V1 Hue bridge.

My Pi is on my LAN, and the Alexa is theoretically supposed to be on the IoT network, meaning that the broadcast would either have to be forwarded between subnets, or I'd have to add the Pi to the IoT network. At the moment, the Alexa is on the LAN so this isn't an issue but moving forward, I want to setup the Pi to have an IP on the IoT network too which only responds to the SSDP broadcasts. There's also a slight issue with this, as the IoT network theoretically has client isolation configured, and SSDP relies on being able to communicate directly.

Regardless, this is a problem for future me... 😎 🔥

Remote Access

I actually had this setup previously but had a problem that I never bothered looking into. As part of OpSec, I probably shouldn't detail this but here we go anyway.

Security through obscurity is not security!

Remote access to my HomeAssistant is via https://ha.chza.me. This is reverse proxied through the VPN host to the Home Assistant host. This worked fine, but all TLS traffic was terminated at the VPN's Apache server, and not tunneled through to the Pi Home Assistant runs on. To replace this, I now terminate TLS at the Pi host, and run a TLS proxy on the VPN machine.

Security

As my server is exposed to the internet, I've got 2FA enforced and a low password attempts counter. The instance should also automatically update to the latest version using Watchtower, to prevent patches being missed.

Under the HTTP module, I specifically ensure that I only allow proxy access from the terminal machine (the networking config will be shared further down), and CORS.

# [...]
http:
  use_x_forwarded_for: true
  cors_allowed_origins:
    - https://ha.home.chza.me
    - https://ha.chza.me
  login_attempts_threshold: 3
  trusted_proxies:
    - 10.5.0.1
# [...]
This sets up CORS properly, and makes sure that any brute force attempts are quickly stopped.

Watchtower is configured to automatically update the container to the latest version every time there is a new version released, and is a good way to run patches on Docker containers:

# [...]
watchtower:
  image: containrrr/watchtower
  container_name: watchtower
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
  command: homeassistant
# [...]

TLS Certificates

Ideally, I wanted to stop using https://running-lion.home.chza.me:8123/ to access Home Assistant, so to force myself to do this, I rebound port 8123 to localhost only, and have to use Apache to reverse proxy all connections. The only insecure connections remaining are port 80 set to accept and reverse proxy the SSDP-discovered Hue bridge endpoints, so a VirtualHost with ServerName ha.home.chza.me exists for access to Home Assistant locally, verified with a DNS challenge based certificate from Let's Encrypt. I use acme-dns for this, as opposed to issuing wide-ranging API credentials on my DNS root (future blog post TBA)!

For this to work and to enable local access whilst at home, I configured two DNS records:

  • ha.home.chza.me, which resolves internally and uses the router for DNS (verified using ACME DNS)
  • ha.chza.me, which is the reverse proxy, publicly accessible (verified with HTTP)

I can then see at a glance if connected locally, or via the internet. There is probably a better way to do this with one DNS record and two certificates; I'll change this in the future if it becomes a problem.

This is a bit clunky as using a reverse proxy requires the X-Forwarded-For headers, which in turn have trusted_proxies, which in turn requires me to specify the proxies I am happy to route through, which changes every time docker-compose creates the network. Therefore, I have to add network configuration to the docker-compose.yml file to ensure the bridge network always has the same gateway. Added configuration includes:

version: '3'
services:
  homeassistant:
    # [...]
    ports:
      - "127.0.0.1:8180:8180" # Emulated Hue, Apache reverse proxies this
      - "127.0.0.1:8123:8123" # Web UI, Apache reverse proxies this too
    # [...]
    networks:
      - hass
networks:
  hass:
    driver: bridge
    ipam:
      config:
        - subnet: 10.5.0.0/24
          gateway: 10.5.0.1 # Set the X-Forwarded-For header consistently

Scenes

Matching Computer Brightness to Current Scene

With a combination of scenes and a couple of custom scripts on my computer, it is possible to setup a time-based lighting, with a 'night shift' for my monitors. The monitors are controlled in hardware through a utility called ddcutil, which can change brightness, inputs, and much more by interacting with the serial connection to the monitor. The underlying serial connection is called DDC/CI and is an I2C bus, implemented on most recent monitors.

This utility allows me to adjust the physical backlight on the monitor, setting it to about 90% for daytime use, 50% after work, 20% in the evening, and 1% at night (note this is nonlinear). I can combine this with the color temperature of the lighting, which is 6500K during the daytime and normal modes, then shifts to 4000K in evening mode, and finally to 2000K at night. I control this with the gamma ramps from Xserver, through the redshift utility.

Combined with night shift on my phone, and a set of timers, I can gently coerce myself to bed at an appropriate time each evening, without blasting the blue light and inhibiting the body's natural sleep response.

To get this working, I have to configure a couple of things: first, the host computer has a convenience script homeassistant-set, which calls my other utility scripts to interact with ddcutil and redshift. This is simply a shell script, adapted to accept the SSH_ORIGINAL_COMMAND if triggered with SSH's ForcedCommand or command="" ssh-ed25519 fgldksfgkldkfgjsdlkf restrictions. Home Assistant can SSH1 into remote computers.

#!/bin/bash

# Enforcing strict error handling
set -Eeuo pipefail

# A greeting to signal the script execution
echo 'homeassistant-set <Bright|Normal|Evening|Night>'

# Combine the first argument from the script or SSH_ORIGINAL_COMMAND
arg="${1:-${SSH_ORIGINAL_COMMAND:-}}"

# If no argument was passed, exit with an error message
if [[ -z "$arg" ]]; then
  echo "Error: No mode argument supplied. Exiting."
  exit 1
fi

# Environment variable required for xrandr setting to work
export DISPLAY=:0

# Print the argument being used
echo "Setting brightness based on mode: $arg"

# Setting brightness of monitors based on the argument
case "$arg" in
  Bright)
    notify-send "Bright mode toggled"
    ~/.local/bin/darkmonitors 90
    redshift -x
    ;;
  Normal)
    notify-send "Normal mode toggled"
    ~/.local/bin/darkmonitors 50
    redshift -x
    ;;
  Evening)
    notify-send "Evening mode toggled"
    ~/.local/bin/darkmonitors 20
    redshift -P -O 4000
    ;;
  Night)
    notify-send "Night mode toggled"
    ~/.local/bin/darkmonitors 1
    redshift -P -O 2000
    ;;
  *)
    echo "Error: Invalid mode argument. Allowed values are Bright, Normal, Evening, Night."
    exit 1
    ;;
esac

Essentially, redshift -x clears ramps, whilst -P -O temp resets gamma ramps, then applies the colour temperature I want, exiting straight after.

darkmonitors is a really simple wrapper round ddcutil:

#!/bin/zsh

ddcutil --display 1 setvcp 10 $1
ddcutil --display 2 setvcp 10 $1
ddcutil --display 3 setvcp 10 $1

Finally, on the host computer, I add a key generated by HA to my authorized_keys file, note the forced command=:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILyHf/jnWpXHGJn/SmJwTvG5YVJne4h/PTYST+I91H3y charlie@chza.me
command="~/.local/bin/homeassistant-set" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJPkhyS5I4EB7rLcV1LNrXfp0M8S2rbf5a2uLYwl4eIz HomeAssistant

At the other end, I generate the public key in the HA config directory, e.g., ssh-keygen -f /config/.ssh/id_ed25519, and create a shell_command2 in the configuration.yaml file:

shell_command:
  set_computer_brightness: >
    ssh
    -oUserKnownHostsFile=/config/.ssh/known_hosts    # Check remote host key
    -i /config/.ssh/id_ed25519                       # Use generated id key from earlier
    -oConnectTimeout=1                               # Don't wait on a downed host
    charlie@chza.home.chza.me                        # Remote computer (my host)
    {{states("input_select.computer_display_mode")}} # Variable template configured to variable helper

This will automatically run the set-homeassistant command from earlier, passing the value of a helper3. This helper is set whenever a scene is activated, and the helper will trigger this shell_command whenever it is changed.

At the moment, the helper doesn't reset state or sync on login to the computer, but theoretically it could poll the HA API on login and sync then. At the moment, it also only works on the Linux box, so won't sync to my Windows boot or things like my work laptop. It also doesn't currently reset the brightness on logout, but this should just be a systemd service.

Time Helpers

Historically, I hardcoded times for various services to activate. This meant if I wanted to change an automation time, I would have a lot to do. I now keep all this in helpers, and then have a box to change the helpers on my main page:

Helper variables and their current settings

Scenes are currently activated at predefined times of the day and don't take into account that I might've gone to bed early, or overridden the scene. Future me needs to look at only applying the scene if I've not manually overridden it.

Conclusion

Anyway, that's this week's post on putting out fires I have previously set for myself. Add my blog to your feed reader to keep up to date with the latest I'm up to and roughly weekly updates on tech, travel, or anything I find interesting. Hopefully I'll release another post about Home Assistant soon™, but in the meantime, check out my last post about HA.

Comments