Post

Integrating the Ikea Starkvind with Home Assistant using deCONZ

Integrating the Ikea Starkvind with Home Assistant using deCONZ

Ikea Starkvind and Home Assistant

We recently bought an Ikea Starkvind Air Purifier, which supports Zigbee. I wanted to find out what I could do with it from within Home Assistant, possibly automating when it runs and when not. I also wanted to add some UI elements, like the mushroom fan card or the air purifier card, both of which rely on there being a fan entity.

Zigbee integration with deCONZ

I already had a ConbeeII Zigbee USB controller in use with the deCONZ Integration. Pairing the Starkvind was a matter of telling the Phoscon Software (which comes with the deCONZ integration) to scan for new sensors and pushing the pairing button on the Starkvind.

Surprisingly enough, only three entities showed up in Home Assistant:

  • Air Purifier PM25
  • Fan Mode
  • Filter Runtime

The PM2.5, Configuration and Filter Time entities made available through deCONZ The PM2.5, Configuration and Filter Time entities made available through deCONZ deCONZ entities exposed through Home Assistant for the Starkvind Air Purifier

When looking in the deCONZ application there were a lot more attributes:

deCONZ cluster screenshot deCONZ cluster screenshot deCONZ cluster information

The deCONZ integration uses a python library for deconz, and in issue #322 I found that only these three items were actually requested to be added. I have since requested some more, but it’s uncertain when and if those will be made available.

I came across this blog post by OyWin, detailing how they used the REST sensors to add their Starkvind into Home Assistant. While the approach was definitely the right way to go, I was not a fan of doing so many individual REST calls (one per sensor) as it’s not needed - Home Assistant can handle it in 1 call per REST-API target.

deCONZ REST-API

Checking the deCONZ REST-API documentation for the Starkvind, there are a lot more attributes available, published under different devices: ZHAAirPurifier and ZHAParticulateMatter

The ones I wanted were:

ZHAAirPurifier

SectionAttributeExposed via deCONZ integrationR/O or R/W
ConfigfilterlifetimexRead/Write
ConfigledindicationxRead/Write
ConfiglockedxRead/Write
ConfigmodeRead Write
ConfigonxRead Only
StatedeviceruntimexRead Only
StatefilterruntimeRead Only
StatelastupdatedxRead Only
StatereplacefilterxRead Only
StatespeedxRead Only

ZHAParticulateMatter

SectionAttributeExposed via deCONZ integrationR/O or R/W
Statemeasured_valueRead Only
StateairqualityxRead Only

Time to get those into Home Assistant.

Configuring the deCONZ REST-API port

In order to be able to query the deCONZ REST-API, you need to make sure a port is configured in Home Assistant → Settings → Apps → deCONZ → Configuration deCONZ Network Configuration screenshot deCONZ Network Configuration screenshot deCONZ App Network Configuration

If this port is set you’ll be able to issue http queries to the URL of your Home Assistant installation on the port specified. In my case this is http://home-assistant.internal:40850/

Finding the correct API urls

To find the correct API endpoints to use go to Home Assistant → deCONZ → Phoscon. Open the hamburger menu on the left and pick Help → API Information Phoscon API Information screenshot Phoscon API Information screen

In the subsequent screen, pick “Sensors” and look for “Ikea STARKVIND Air Purifier”. You should find two entries in the dropdown: Phoscon API Information - Starkvind Phoscon API Information for the Starkvind Air Purifier

Once you click on one of the sensors, you will get a dump of what the API returns, and on top of that window, the API endpoint URL. In my example this reads:

//home-assistant.internal:8123/api/hassio_ingress/juXMtc1g4Z85iNwXSis58q2z7Kw7XO0Lz5k2X6cBsZ0/api/792DA42905/sensors/93. The converted direct unauthenticated URL becomes http://home-assistant.internal:40850/api/792DA42905/sensors/93. Phoscon API Information - Starkvind ZHAAirPurifier Phoscon API information for the ZHAAirPurifier entity of the Starkvind Air Purifier

792DA42905 is your own API key, and 93 is the internal numbering of deCONZ for your sensor.

Now, this URL allows you to query the API from the outside. I did not need this as I wanted to run the queries from inside Home Assistant. You can find the internal url by going to Home Assistant → Settings → Devices & services, selecting the deCONZ integration and picking the Conbee2. In the Service Info there is a “Visit” link, which shows you the internal hostname to use. deCONZ Conbee2 Service Info screenshot deCONZ Conbee2 Service Info screenshot deCONZ Conbee2 Service Information

This will usually be core-deconz, so the URL becomes http://core-deconz:40850/api/<apikey>/sensors/<sensor-id>.

Home Assistant Configuration

Creating the REST sensors

Using the URL assembled above I added the sensor and binary_sensor entities to Home Assistant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
- rest:
  - resource: http://core-deconz:40850/api/792DA42905/sensors/93
    binary_sensor:
      - name: Ikea Starkvind Led Indication
        value_template: "{{ value_json.config.ledindication }}"
        unique_id: ikea_starkvind_led_indication
  
      - name: Ikea Starkvind Locked
        value_template: "{{ value_json.config.locked }}"
        unique_id: ikea_starkvind_locked
  
      - name: Ikea Starkvind Sensor On
        value_template: "{{ value_json.config.on }}"
        unique_id: ikea_starkvind_sensor_on
  
      - name: Ikea Starkvind Replace Filter
        value_template: "{{ value_json.state.replacefilter }}"
        unique_id: ikea_starkvind_replace_filter
  
    sensor:
      - name: Ikea Starkvind Filter Runtime
        value_template: "{{ value_json.state.filterruntime }}"
        unique_id: ikea_starkvind_filter_runtime
        device_class: duration
        unit_of_measurement: min
  
      - name: Ikea Starkvind Device Runtime
        value_template: "{{ value_json.state.deviceruntime }}"
        unique_id: ikea_starkvind_device_runtime
        device_class: duration
        unit_of_measurement: min
  
      - name: Ikea Starkvind Filter Lifetime
        value_template: "{{ value_json.config.filterlifetime }}"
        unique_id: ikea_starkvind_filter_lifetime
        device_class: duration
        unit_of_measurement: min
  
      - name: Ikea Starkvind Mode
        value_template: "{{ value_json.config.mode }}"
        unique_id: ikea_starkvind_mode
  
      - name: Ikea Starkvind Fan Speed
        value_template: "{{ value_json.state.speed }}"
        unique_id: ikea_starkvind_fan_speed
        state_class: measurement
  
      - name: Ikea Starkvind Last Updated
        value_template: "{{ value_json.state.lastupdated + 'Z' }}"
        unique_id: ikea_starkvind_lastupdated
        device_class: timestamp

Enabling setting the led indicator

To be able to update the ledindication, I added a binary helper called ikea_starkvind_ledindication as a toggle, a rest_command to set it, and an automation to bind the two together:

1
2
3
4
5
6
7
8
rest_command:
  ikea_starkvind_set_ledindication:
    url: "http://core-deconz:40850/api/792DA42905/sensors/93"
    method: put
    content_type: "application/json; charset=utf-8"
    payload: '{ "config": { "ledindication": {{ states("input_boolean.ikea_starkvind_ledindication") | bool | lower }}}}'

1
2
3
4
5
6
7
8
9
10
alias: Ikea Starkvind - Sync Led Indication
description: ""
triggers:
  - trigger: state
    entity_id:
      - input_boolean.ikea_starkvind_ledindication
actions:
  - action: rest_command.ikea_starkvind_set_ledindication
    data: {}
mode: single

Additional sensors

I also added a template sensor to calculate the lifetime left of the filter:

1
2
3
4
5
6
7
8
9
template:
  - sensor:
      name: Ikea Starkvind Filter Lifetime Remaining
      state: "{{ states('sensor.ikea_starkvind_filter_lifetime') | int - states('sensor.ikea_starkvind_filter_runtime') | int }}"
      unique_id: ikea_starkvind_filter_lifetime_remaining
      device_class: duration
      unit_of_measurement: min

Creating a Fan entity

To use the premade cards I needed a fan entity. This can be created as a template, based off of the previously created entities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
- fan:
    - name: "IKEA Starkvind"
      unique_id: ikea_starkvind_fan
      availability: "{{ states('select.ikea_starkvind_fan_mode') not in ['unknown', 'unavailable'] }}"
      state: "{{ states('select.ikea_starkvind_fan_mode') != 'off' }}"
      percentage: >
        {% set map = {
          'speed_1': 20, 'speed_2': 40, 'speed_3': 60,
          'speed_4': 80, 'speed_5': 100
        } %}
        {{ map.get(states('select.ikea_starkvind_fan_mode'), 0) }}
      preset_mode: >
        {% if states('select.ikea_starkvind_fan_mode') == 'auto' %}auto{% endif %}
      preset_modes:
        - auto
      speed_count: 5
      turn_on:
        action: select.select_option
        target:
          entity_id: select.ikea_starkvind_fan_mode
        data:
          option: auto
      turn_off:
        action: select.select_option
        target:
          entity_id: select.ikea_starkvind_fan_mode
        data:
          option: "off"
      set_percentage:
        action: select.select_option
        target:
          entity_id: select.ikea_starkvind_fan_mode
        data:
          option: >
            {% if percentage == 0 %} off
            {% elif percentage <= 20 %} speed_1
            {% elif percentage <= 40 %} speed_2
            {% elif percentage <= 60 %} speed_3
            {% elif percentage <= 80 %} speed_4
            {% else %} speed_5
            {% endif %}
      set_preset_mode:
        action: select.select_option
        target:
          entity_id: select.ikea_starkvind_fan_mode
        data:
          option: "{{ preset_mode }}"

Home Assistant Cards

I tried out a few cards to see what I liked.

Custom Purifier Card

My first try was the custom air purifier card with this configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type: custom:purifier-card
entity: fan.ikea_starkvind
aqi:
  entity_id: sensor.ikea_starkvind_air_quality_measured_value
  unit: μg/m³
stats:
  - entity_id: sensor.ikea_starkvind_filter_lifetime_remaining
    value_template: "{{ (value | float(0) / 60 / 24 ) | round(1) }}"
    unit: days
    subtitle: Filter Life Remaining
shortcuts:
  - name: Speed 1
    icon: mdi:weather-night
    percentage: 20
  - name: Speed 2
    icon: mdi:circle-slice-2
    percentage: 40
  - name: Speed 3
    icon: mdi:circle-slice-4
    percentage: 60
  - name: Speed 4
    icon: mdi:circle-slice-6
    percentage: 80
  - name: Speed 5
    icon: mdi:circle-slice-8
    percentage: 100
  - name: Auto
    icon: mdi:brightness-auto
    preset_mode: auto

It ended up looking like this, which did not thrill me.

Custom Purifier Card Custom Purifier Card Air Purifier Card

Custom cards

I cobbled something together based on the mushroom-fan-card, the button-card, the template-entity-row and the lovelace-expander-card.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
type: vertical-stack
cards:
  - type: custom:mushroom-fan-card
    entity: fan.ikea_starkvind
    icon_animation: true
    primary_info: name
    secondary_info: state
    show_percentage_control: true
    collapsible_controls: true
    show_direction_control: false
    show_oscillate_control: false
  - type: horizontal-stack
    cards:
      - type: custom:button-card
        icon: mdi:circle-outline
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 0
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'off'
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'off'
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
      - type: custom:button-card
        icon: mdi:weather-night
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 20
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 20
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 20
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
      - type: custom:button-card
        icon: mdi:circle-slice-2
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 40
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 40
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 40
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
      - type: custom:button-card
        icon: mdi:circle-slice-4
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 60
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 60
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 60
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
      - type: custom:button-card
        icon: mdi:circle-slice-6
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 80
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 80
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 80
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
      - type: custom:button-card
        icon: mdi:circle-slice-8
        entity: fan.ikea_starkvind
        show_name: false
        aspect_ratio: 1/1
        tap_action:
          action: call-service
          service: fan.set_percentage
          data:
            percentage: 100
          target:
            entity_id: fan.ikea_starkvind
        styles:
          card:
            - background-color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 100
                    ? 'rgba(var(--rgb-state-active-color), 0.2)'
                    : 'var(--ha-card-background)';
                ]]]
          icon:
            - color: |-
                [[[
                  return entity.state === 'on' && entity.attributes.percentage === 100
                    ? 'var(--state-active-color)'
                    : 'var(--secondary-text-color)';
                ]]]
  - type: custom:expander-card
    title: Details
    cards:
      - type: entities
        entities:
          - entity: sensor.ikea_starkvind_last_updated
            name: Last Updated
          - entity: input_boolean.ikea_starkvind_ledindication
            name: Led Indicator
          - entity: sensor.ikea_starkvind_air_quality
            name: Air Quality Indicator
          - type: custom:template-entity-row
            state: >-
              {{ states('sensor.ikea_starkvind_air_quality_measured_value') }}
              µg/m³
            name: Air Quality
            icon: mdi:bacteria-outline
          - entity: binary_sensor.ikea_starkvind_replace_filter
            name: Replace Filter
          - type: custom:template-entity-row
            name: Filter Life Remaining
            state: |-
              {{ (states('sensor.ikea_starkvind_filter_lifetime_remaining') |
                  float(0) / 60 / 24)| round(1) }} days
            icon: mdi:timelapse
    animation: false
    clear-children: false

This one I kept :)

Custom card configuration Custom card configuration Collapsed custom card

Custom card configuration Custom card configuration Unfolded custom card

This post is licensed under CC BY 4.0 by the author.