Thanks, @Mariusthvdb, @RomRider and all the other here for all the great help and support. The custom:button-card
really is amazing. I combined it with @thomasloven custom:state-switch
and custom:card-mod
to leverage @betaboonās fresh attempt at integrating the Philips air purifiers. Adding on top some self-made icons (what an act to get that right). Here is what my card looks like:
High air quality, auto fan speed, display light on and humidification activated |
Somewhat lower air quality, night mode, display light off and humidification deactivated |
|
|
The glowing ring follows the Philips design of indicating air freshness according to IAI values. It changes it color with air quality. The icons on top signal when filter changes are needed or the water of the humidifier is low. Note that some of those are deactivated when the humidification is switched off. They change from green to orange to red (when they also start blinking). The readings in the middle give me some stats of the device. Nothing is clickable in the upper part.
The four buttons are a bit Homekit style and control the device. They can have a combination of states: they all go dark when off, but some also cycle through the modes of, e.g. fan speed. The cycling changes the icons. Here is where I struggled quite a bit with state logic in combination with templating.
Overall, I made quite some use of the button-card
templates and variables, which came in very handy.
Here is how I did it:
In the configuration.yaml I integrate the air purifier using the fork of @betaboon as mentioned above (it works better with encrypted coap devices):
# Philips Air Purifier
fan:
- platform: philips_airpurifier
host: !secret philips_ac2889_host
model: ac2889
name: air_ac2889
- platform: philips_airpurifier
host: !secret philips_ac2729_host
model: ac2729
name: air_ac2729
As I have two devices, I tried to develop the card in a flexible way that can deal with both devices gracefully, even though the AC2889 doesnāt have the humidification function. That requires a bit of testing and switching. Also, the entity
variable comes in quite handy to keep it abstract, as youāll see below. Iāll show the code for the AC2729 device which corresponds to the screenshots above. The main card essentially is a vertical-stack
with button-card
s inside:
type: vertical-stack
layout: horizontal
cards:
- type: 'custom:button-card'
icon: 'custom:circle'
entity: fan.air_ac2729
show_name: false
template: ac_state
size: 100%
state:
- operator: template
value: '[[[ return variables.has_iai <= 3 ]]]'
color: blue
- operator: template
value: '[[[ return variables.has_iai <= 6 ]]]'
color: '#6666ff'
- operator: template
value: '[[[ return variables.has_iai <= 9 ]]]'
color: '#ff00ff'
- operator: template
value: '[[[ return variables.has_iai > 9 ]]]'
color: red
styles:
icon:
- filter: drop-shadow(0 0 0.7rem)
card:
- pointer-events: none
grid:
- position: relative
custom_fields:
primary_reading:
- position: absolute
- top: 50%
- left: 50%
- transform: 'translate(-50%, -60%)'
- pointer-events: none
secondary_reading:
- position: absolute
- top: 57%
- left: 50%
- transform: 'translate(-50%, 0%)'
- pointer-events: none
notifications:
- position: absolute
- top: 25%
- left: 50%
- transform: 'translate(-50%, 0%)'
- pointer-events: none
custom_fields:
notifications:
card:
type: horizontal-stack
cards:
- type: 'custom:button-card'
template: ac_notifications
icon: 'mdi:air-filter'
color: darkgreen
state:
- operator: template
value: '[[[ return variables.has_days_pre <=1 ]]]'
styles:
icon:
- color: darkred
- animation: blink 3s ease infinite
- operator: template
value: '[[[ return variables.has_days_pre <=3 ]]]'
styles:
icon:
- color: darkorange
- type: 'custom:button-card'
template: ac_notifications
icon: 'philips:filter_replacement'
color: darkgreen
state:
- operator: template
value: '[[[ return variables.has_days_hepa <=1 ]]]'
styles:
icon:
- color: darkred
- animation: blink 3s ease infinite
- operator: template
value: '[[[ return variables.has_days_hepa <=3 ]]]'
styles:
icon:
- color: darkorange
- type: 'custom:button-card'
template: ac_notifications
icon: 'mdi:image-filter-none'
color: darkgreen
state:
- operator: template
value: '[[[ return variables.has_days_carbon <=1 ]]]'
styles:
icon:
- color: darkred
- animation: blink 3s ease infinite
- operator: template
value: '[[[ return variables.has_days_carbon <=3 ]]]'
styles:
icon:
- color: darkorange
- type: 'custom:state-switch'
entity: template
template: '[[[ return variables.has_water >= 0 && !variables.is_pur ]]]'
states:
'true':
type: 'custom:button-card'
template: ac_notifications
icon: 'philips:water_refill'
color: darkgreen
state:
- operator: template
value: '[[[ return variables.has_water <=10 ]]]'
styles:
icon:
- color: darkred
- animation: blink 3s ease infinite
- operator: template
value: '[[[ return variables.has_water <=30 ]]]'
styles:
icon:
- color: darkorange
- type: 'custom:state-switch'
entity: template
template: '[[[ return variables.has_days_wick >= 0 && !variables.is_pur ]]]'
states:
'true':
type: 'custom:button-card'
template: ac_notifications
icon: 'philips:prefilter_wick_cleaning'
color: darkgreen
state:
- operator: template
value: '[[[ return variables.has_days_wick <=1 ]]]'
styles:
icon:
- color: darkred
- animation: blink 3s ease infinite
- operator: template
value: '[[[ return variables.has_days_wick <=3 ]]]'
styles:
icon:
- color: darkorange
secondary_reading:
card:
type: vertical-stack
cards:
- type: 'custom:button-card'
template: ac_readings
icon: |
[[[
if (variables.is_pm25)
return 'philips:iai'
else
return 'philips:pm25'
]]]
variables:
var_reading_text: |
[[[
if (variables.is_pm25)
return variables.has_iai
else
return variables.has_pm25
]]]
- type: 'custom:state-switch'
entity: template
template: '[[[ return variables.has_temp >= 0 ]]]'
states:
'true':
type: 'custom:button-card'
template:
- ac_readings
- ac_state
icon: 'hass:thermometer'
variables:
var_reading_text: '[[[ return variables.has_temp + " Ā°C" ]]]'
- type: 'custom:state-switch'
entity: template
template: '[[[ return variables.has_humidity >= 0 ]]]'
states:
'true':
type: 'custom:button-card'
template: ac_readings
icon: 'philips:humidity_button'
variables:
var_reading_text: '[[[ return variables.has_humidity + " %" ]]]'
primary_reading:
card:
type: 'custom:button-card'
icon: |
[[[
if (variables.is_pm25)
return 'philips:pm25'
else
return 'philips:iai'
]]]
size: 50%
label: |
[[[
if (variables.is_pm25)
return variables.has_pm25
else
return variables.has_iai
]]]
layout: icon_label
show_label: true
styles:
label:
- font-size: 400%
style: |
ha-card {
box-shadow: none;
background: none;
}
- type: horizontal-stack
cards:
- type: 'custom:button-card'
template: buttons
icon: 'philips:power_button'
entity: fan.air_ac2729
name: Power
tap_action:
action: call-service
service: script.toggle_ac_power
service_data:
entity_id: entity
- type: 'custom:button-card'
template:
- buttons
- ac_state
entity: fan.air_ac2729
name: LĆ¼fter
tap_action:
action: call-service
service: script.toggle_ac_fan_speed
service_data:
entity_id: entity
state:
- operator: template
value: '[[[ return variables.is_off ]]]'
styles:
card:
- pointer-events: none
- operator: template
value: '[[[ return variables.is_on ]]]'
id: activated_state
icon: |-
[[[
switch (variables.has_speed) {
case "1":
return "philips:fan_speed_button"
case "2":
return "philips:fan_speed_button"
case "3":
return "philips:fan_speed_button"
case "auto":
return "philips:auto_mode"
case "allergen":
return "philips:allergen_mode"
case "night":
return "philips:sleep_mode"
case "turbo":
return "philips:fan_speed_button"
}
]]]
styles:
icon:
- animation: |-
[[[
var speed = 'linear 0s infinite normal none running rotating';
switch (variables.has_speed) {
case '1':
return '12s ' + speed;
case '2':
return '6s ' + speed;
case '3':
return '2s ' + speed;
case 'turbo':
return '0.5s ' + speed;
default:
return 'none';
}
]]]
grid:
- position: relative
custom_fields:
fan_speed:
- position: absolute
- top: 47%
- left: 42%
- transform: 'translate(-50%, -60%)'
- pointer-events: none
custom_fields:
fan_speed:
card:
type: 'custom:button-card'
template: activated_state
color_type: icon
show_icon: false
show_label: true
label: |-
[[[
switch (variables.has_speed) {
case '1':
return '1';
case '2':
return '2';
case '3':
return '3';
case 'turbo':
return 't';
}
]]]
state:
- operator: template
value: '[[[ return variables.is_on ]]]'
id: activated_state
styles:
label:
- padding: 2px 6px 0px 6px
- font-weight: bold
- color: |
[[[
if (variables.is_off)
return 'var(--paper-item-icon-color)';
else
return 'var(--mmp-accent-color, var(--accent-color))';
]]]
style: |
ha-card {
box-shadow: none;
}
- type: 'custom:button-card'
template:
- buttons
- ac_state
icon: 'philips:light_dimming_button'
entity: fan.air_ac2729
name: Anzeige
tap_action:
action: call-service
service: script.toggle_ac_light
service_data:
entity_id: entity
state:
- operator: template
value: '[[[ return variables.is_off ]]]'
styles:
card:
- pointer-events: none
- operator: template
value: '[[[ return variables.is_backlight ]]]'
id: activated_state
- type: 'custom:button-card'
template:
- buttons
- ac_state
entity: fan.air_ac2729
name: Modus
tap_action:
action: call-service
service: script.toggle_ac_two_in_one_mode
service_data:
entity_id: entity
state:
- operator: template
value: '[[[ return variables.is_on && variables.has_humidity > 0 ]]]'
icon: >-
[[[ return variables.is_pur ? "philips:purification_only_mode" :
"philips:two_in_one_mode" ]]]
id: activated_state
- operator: template
value: '[[[ return variables.is_off || variables.has_humidity < 0 ]]]'
icon: >-
[[[ return variables.is_pur ? "philips:purification_only_mode" :
"philips:two_in_one_mode" ]]]
styles:
card:
- pointer-events: none
Youāll notice that it uses templates and variables. These are put into the lovelace raw configuration:
button_card_templates:
activated_state:
state:
- id: activated_state
styles:
card:
- background-color: var(--primary-text-color)
- color: var(--card-background-color)
name:
- color: 'var(--mmp-accent-color, var(--accent-color))'
- font-weight: bold
label:
- color: 'var(--mmp-accent-color, var(--accent-color))'
state:
- color: 'var(--mmp-accent-color, var(--accent-color))'
icon:
- color: 'var(--mmp-accent-color, var(--accent-color))'
raw_buttons:
template: activated_state
aspect_ratio: 1/1
color: var(--paper-item-icon-color)
tap_action:
action: toggle
haptic: medium
state:
- value: 'on'
id: activated_state
styles:
card:
- '--mdc-ripple-color': black
- '--mdc-press-opacity': 0.5
- font-size: var(--paper-font-body1_-_font-size)
- padding-bottom: 10px
buttons:
template: raw_buttons
show_name: true
show_label: true
show_state: false
color_type: icon
styles:
grid:
- grid-template-areas: '"i" "l" "n"'
- grid-template-columns: 1fr
- grid-template-rows: 4fr 1fr 1fr
img_cell:
- align-self: start
- text-align: start
- justify-content: start
- padding-left: 16px
name:
- justify-self: start
- padding-left: 10px
- font-weight: bold
label:
- justify-self: start
- padding-left: 10px
state_buttons:
template: buttons
show_name: false
show_state: true
styles:
grid:
- grid-template-areas: '"i" "l" "s"'
state:
- justify-self: start
- padding-left: 10px
- font-weight: bold
ac_state:
variables:
is_on: '[[[ return entity && entity.state && entity.state == "on" ]]]'
is_off: '[[[ return entity && entity.state && entity.state == "off" ]]]'
is_pur: |-
[[[ return entity && entity.attributes && entity.attributes.function &&
entity.attributes.function == "Purification"
]]]
is_backlight: |-
[[[ return entity && entity.attributes && entity.attributes.function &&
entity.attributes.display_backlight > 0
]]]
is_pm25: >-
[[[ return entity && entity.attributes &&
entity.attributes.preferred_index &&
entity.attributes.preferred_index == "PM2.5"
]]]
has_speed: |-
[[[ return (entity && entity.attributes && entity.attributes.speed) ?
entity.attributes.speed : null
]]]
has_iai: |-
[[[
return (entity && entity.attributes ) ?
entity.attributes.indoor_allergen_index : -1
]]]
has_pm25: |-
[[[
return (entity && entity.attributes ) ?
entity.attributes.pm25 : -1
]]]
has_temp: |-
[[[
return (entity && entity.attributes ) ?
entity.attributes.temperature : -1
]]]
has_humidity: |-
[[[
return (entity && entity.attributes ) ?
entity.attributes.humidity : -1
]]]
has_days_pre: |-
[[[
return (entity && entity.attributes ) ?
(entity.attributes.filter_pre_remaining).match(/\d+/i) : -1
]]]
has_days_hepa: |-
[[[
return (entity && entity.attributes ) ?
(entity.attributes.filter_hepa_remaining).match(/\d+/i) : -1
]]]
has_days_carbon: |-
[[[
return (entity && entity.attributes ) ?
(entity.attributes.filter_active_carbon_remaining).match(/\d+/i) : -1
]]]
has_days_wick: |-
[[[
return (entity && entity.attributes ) ?
(entity.attributes.filter_wick_remaining).match(/\d+/i) : -1
]]]
has_water: |-
[[[
return (entity && entity.attributes ) ?
entity.attributes.water_level : -1
]]]
ac_readings:
variables:
var_name: var_reading_text
label: |
[[[ return variables.var_reading_text ]]]
layout: icon_label
show_label: true
size: 100%
styles:
label:
- font-size: 100%
card:
- width: 100px
- height: 30px
- box-shadow: none
- background: none
ac_notifications:
layout: vertical
show_label: false
show_name: false
color: red
size: 100%
styles:
card:
- width: 50px
- height: 30px
- box-shadow: none
- background: none
label_card:
styles:
card:
- background: none
- box-shadow: none
- pointer-events: none
name:
- font-size: 'var(--ha-card-header-font-size, 24px)'
- justify-self: start
- padding-left: 10px
To make it work, I need a few scripts:
Toggeling power takes care of restoring the display light setting (the device insists on switching the light back on when it is powered on regardless of previous state):
alias: Toggle AC power
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ is_state(entity_id, "off") }}'
sequence:
- service: fan.turn_on
data:
entity_id: '{{ entity_id }}'
- choose:
- conditions:
- condition: template
value_template: '{{ light == False }}'
sequence:
- delay: '1'
- service: philips_airpurifier.set_light_brightness
data:
entity_id: '{{ entity_id }}'
brightness: 0
- delay: '1'
- service: philips_airpurifier.set_display_backlight_off
data:
entity_id: '{{ entity_id }}'
default:
- service: fan.turn_off
data:
entity_id: '{{ entity_id }}'
mode: single
icon: 'philips:power_button'
fields:
entity_id:
description: Entity of fan
example: fan.air_ac2729
description: Toggle the power of Philips AC
variables:
light: '{{ state_attr(entity_id, "display_backlight") }}'
Toggeling the light is required as two settings have to be set:
alias: Toggle AC light
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ is_state(entity_id, "on") }}'
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ is_state_attr(entity_id, ''display_backlight'', true) }}'
sequence:
- service: philips_airpurifier.set_display_backlight_off
data:
entity_id: '{{ entity_id }}'
- service: philips_airpurifier.set_light_brightness
data:
entity_id: '{{ entity_id }}'
brightness: 0
default:
- service: philips_airpurifier.set_display_backlight_on
data:
entity_id: '{{ entity_id }}'
- service: philips_airpurifier.set_light_brightness
data:
entity_id: '{{ entity_id }}'
brightness: 100
mode: single
icon: 'philips:light_dimming_button'
fields:
entity_id:
description: Entity of fan
example: fan.air_ac2729
description: Toggle the backlight of philips AC
Toggeling the purification mode is rather simple, it can only take two states:
alias: Toggle AC 2in1 mode
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ states[entity_id].attributes.function == ''Purification''}}'
sequence:
- service: philips_airpurifier.set_function
data:
entity_id: '{{ entity_id }}'
function: purification_humidification
default:
- service: philips_airpurifier.set_function
data:
entity_id: '{{ entity_id }}'
function: purification
mode: single
icon: 'philips:two_in_one_mode_button'
fields:
entity_id:
description: Entity of fan
example: fan.air_ac2729
description: Toggle the backlight of philips AC
What took me a while is to figure out how to cycle through an attribute state list with multiple states when repeatedly clicking a button - while skipping the first state as this switches the device off:
(updated from the original script for the new fan model in HA introducing preset_mode
. Note, for this to work you also need at least version 0.4.0 of the custom integration: GitHub - betaboon/philips-airpurifier-coap: šØ Philips AirPurifier custom component for Home Assistant. With support for new Devices with CoAP protocol. Tested on AC2729/10 (bought early 2020) )
alias: Toggle AC fan speed
sequence:
- service: fan.set_preset_mode
data_template:
entity_id: '{{ entity_id }}'
preset_mode: >
{%- set current = state_attr(entity_id, 'preset_mode') %}
{%- set modes = state_attr(entity_id, 'preset_modes') %}
{%- set index = modes.index(current) %}
{%- set next = 0 if current == modes[-1] else index + 1 %}
{{ modes[next] }}
mode: single
icon: 'philips:fan_speed_button'
fields:
entity_id:
description: Entity of fan
example: fan.air_ac2729
description: Set the speed mode of philips AC
Maybe this can serve someone as help and inspiration. Iām sure some things could have been done more elegantly. I save you the code for the custom icons, as this is a separate can of worms. However, I documented my approach here.