Adding a Capacity Provider to an ECS Cluster with Terraform

I recently had to use terraform to add a capacity provider to an existing ECS cluster.

After adding a default capacity provider to the cluster, existing services still have a launch_type=EC2, so we need to update them to use a capacity_provider_strategy in order to use it. Unfortunately we can’t do this in terraform due to a long-standing bug:

When an ECS cluster has a default_capacity_provider_strategy setting defined, Terraform will mark all services that don’t have ignore_changes=[capacity_provider_strategy] to be recreated.

The ECS service actually does support changing from launch_type to capacity_provider_strategy non-destructively, by forcing a redeploy. Since this uses the service’s configured deployment mechanism there’s no disruption.

ECS Compute Configuration

We can also set this using the CLI:

aws ecs update-service --cluster my-cluster --service my-service --capacity-provider-strategy capacityProvider=ec2,weight=100,base=0 --force-new-deployment

If for some reason we need to revert, ECS also supports changing back from capacity_provider_strategy to launch_type, however the option is disabled in the console:

ECS Compute Configuration

As a workaround, we can pass an empty list of capacity providers to the update-service command, which will result in the service using launch_type=EC2 again.

aws ecs update-service --cluster my-cluster --service my-service --capacity-provider-strategy '[]' --force-new-deployment

Home Assistant Apple Watch Complications

I spent a little time recenly working out how to get Home Assistant complications working on my watch. Once you work out the inputs they want they seem to be really reliable, and they also function as a quick way to open the watch app to trigger actions and scenes.

Apple Watch with Home Assistant Complication

It’s important to pick the right complication for the watchface you are using. I used the “Graphic Circular” complication type, with the “Open Gauge Image” template. The Apple Developer Documentation has examples of all the different complication types and which watch faces support them.

Instead of trying to type in the Jinja2 templates on my phone, I found it was much easier to use the developer tools template editor inside Home Assistant. Once it’s working you can just copy and paste it into the mobile app.

We need to use template syntax to generate two values. The first is the sensor value. There is only just enough space for 3 digits, so I’ve rounded the value to 1 decimal place:

{{ states("sensor.office_temperature") | float | round(1) }}

The other value we need is a value between 0.0 and 1.0, representing the percentage that the gauge should be filled. Since I don’t know what the temperature range is going to be, I’m using max() and min() to ensure we always get a value between zero and 1, and that the gauge fills as we approach the target temperature.

{% set current = states("sensor.office_temperature") | float %}
{% set low = min(18.0, current) %}
{% set high = max(22.0, current) %}
{{ (current - low) / (high - low) }}

We also need to specify an icon - I used chair_rolling to match my office dashboard. It’s a bit small but you can more or less make it out.

Syncing an On Call Calendar to Home Assistant

I thought it might be useful if Home Assistant knew when I was on call. I could use this to make sure the office doesn’t get too cold overnight, or to send me a notification if I leave home without my laptop.

Home Assistant Notification

We use PagerDuty, which gives you an iCal calendar feed, so I assumed I could just use this. Unfortunately while Home Assistant has integrations for Local Calendars and CalDAV, neither of these support just fetching a single .ics file over http.

After a bit of digging around I discovered that Home Assistant stores local calendars in the .storage folder alongside its config files, so I figured I can just overwrite this file manually using a shell_command. You need to create the calendar first (under Settings > Devices & Services > Add Integration > Local Calendar). Once it’s created, add an event to get Home Assistant to create the local calendar file.

The shell_command goes into configuration.yaml:

shell_command:
  update_on_call_calendar: 'curl https://pagerduty.com/path/to/calendar > /config/.storage/local_calendar.on_call.ics'

We can then use the shell command in an automation, followed by homeassistant.reload_config_entry to get Home Assistant to reload the file from disk. I have this running on an hourly time_pattern trigger, but you could increase the update frequency for a calendar that changes more regularly.

alias: Refresh On Call Calendar
description: ""
trigger:
  - platform: time_pattern
    minutes: "0"
condition: []
action:
  - service: shell_command.update_on_call_calendar
    data: {}
  - service: homeassistant.reload_config_entry
    target:
      entity_id: calendar.on_call
    data: {}
mode: single

Once the calendar has updated you should see events show up in Home Assistant. The calendar state can be used in automations:

alias: On Call Laptop Check
description: "Send a push notification if I leave my laptop at home when I'm on call"
trigger:
  - platform: state
    entity_id:
      - person.tom_henderson
    to: not_home
condition:
  - condition: and
    conditions:
      - condition: state
        entity_id: calendar.on_call
        state: "on"
      - condition: device
        device_id: <device_id>
        domain: device_tracker
        entity_id: device_tracker.toms_m2
        type: is_home
action:
  - device_id: <device_id>
    domain: mobile_app
    type: notify
    message: You're on-call. Did you leave your laptop at home?
mode: single

Control Sonos with MoesHouse Zigbee Smart Knob

I picked up this neat little Zigbee knob on AliExpress to use as a volume control for Sonos. It seems pretty well made, with a strong magnet on the back to attach it to its little mounting plate (or anything else it’ll stick to) and it was easy enough to connect to Zigbee2MQTT.

MoesHouse Zigbee Smart Knob

Smart Home Scene has a detailed review with more photos and a tear down of the internals.

The only minor gotcha is the need to disable legacy mode for the knob in Zigbee2MQTT, which alters the MQTT payload to send action triggers.

  '0x00123456789abcde':
    friendly_name: 'office-smart-knob'
    legacy: false

To control Sonos I’ve set up three automations in Home Assistant: rotate right / left for volume up and down, and single press to select the line in source. For the volume automations I added a condition to check that the Office Sonos is actually playing, to avoid being deafened after using it as a fidget toy when nothing is playing.

Volume Up / Down Automation

alias: Office Volume Up
trigger:
  - platform: device
    domain: mqtt
    device_id: <knob device id>
    type: action
    subtype: rotate_right
    discovery_id: 0x00123456789abcde action_rotate_right
condition:
  - condition: device
    device_id: <sonos device id>
    domain: media_player
    entity_id: media_player.office
    type: is_playing
action:
  - service: media_player.volume_up
    data: {}
    target:
      device_id: <sonos device id>
mode: single

Source Selection Automation

alias: Office Source to Line-in
trigger:
  - platform: device
    domain: mqtt
    device_id: <knob device id>
    type: action
    subtype: single
    discovery_id: 0x00123456789abcde action_single
condition: []
action:
  - service: media_player.select_source
    data:
      source: Line-in
    target:
      device_id: <sonos device id>
mode: single

Control Sonos from the Menu Bar using Home Assistant and Hammerspoon

We have a Sonos setup at home which I love. In my office I have a Sonos Port connected to my turntable and some powered speakers. I’ve been tinkering with Home Assistant recently, and the Sonos integration works really well, so I decided to build a little menu bar app to pull up my AV dashboard.

Hammerspoon is a great little swiss-army tool for macOS which I already use for various shortcuts and automations. This is what I came up with, using just an hs.menubar with no menu items, and an hs.webview positioned below it to mimic a menu. It seems to work pretty well.

local obj = {}
obj.__index = obj

-- Metadata
obj.name = "Home"

obj.window = hs.webview.new({x=100, y=100, w=400, h=800})
	:url("https://homassistant.local")
    :allowNewWindows(false)
	:allowTextEntry(true)
    :shadow(true)

obj.menu = hs.menubar.new()
	:setTitle("Home")

obj.menu:setClickCallback(function()
    local menuframe = obj.menu:frame()
    local windowframe = obj.window:frame()

    local x = menuframe.x - (windowframe.w / 2) + (menuframe.w / 2)

    obj.window:frame({x=x, y=30, w=400, h=800})

    if obj.window:isVisible() then
        obj.window:hide()
    else
        obj.window:show()
            :windowCallback(function(action, webview, hasFocus)
                if action == "focusChange" and not hasFocus then
                    obj.window:hide()
                end
            end)
            :bringToFront(true)
    end
end)

return obj