From 5393bc77d70371c3cd7be59a7b82f97a5f7efa48 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Mon, 1 Jun 2026 22:19:47 -0700 Subject: [PATCH] feat: add fan controls --- Home_Assistant/blueprints/README.md | 8 + .../hasp_Display_Fan_Speed_with_Icon.yaml | 465 ++++++++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 Home_Assistant/blueprints/hasp_Display_Fan_Speed_with_Icon.yaml diff --git a/Home_Assistant/blueprints/README.md b/Home_Assistant/blueprints/README.md index a37ad6b..f14f552 100644 --- a/Home_Assistant/blueprints/README.md +++ b/Home_Assistant/blueprints/README.md @@ -86,6 +86,14 @@ A HASPone button displays a dimmer control on page 4 and 5 with a toggle on/off --- +### Display Fan Speed with Icon + +[![Display Fan Speed with Icon](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Display_Fan_Speed_with_Icon.yaml) + +A HASPone button displays a fan speed control on page 4 and 5 with a toggle on/off icon to the left. + +--- + ### Display Entity State or Attribute [![Display Entity State or Attribute](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FHASwitchPlate%2FHASPone%2Fblob%2Fmain%2FHome_Assistant%2Fblueprints%2Fhasp_Display_Entity_State_or_Attribute.yaml) diff --git a/Home_Assistant/blueprints/hasp_Display_Fan_Speed_with_Icon.yaml b/Home_Assistant/blueprints/hasp_Display_Fan_Speed_with_Icon.yaml new file mode 100644 index 0000000..946ea7a --- /dev/null +++ b/Home_Assistant/blueprints/hasp_Display_Fan_Speed_with_Icon.yaml @@ -0,0 +1,465 @@ +blueprint: + name: "HASPone p[x].b[y] displays a fan speed control with a toggle on/off icon" + description: | + + ## Blueprint Version: `1.08.00` + + # Description + + A HASPone button displays a fan speed control on page 4 and 5 with a toggle on/off icon to the left. + + ## HASPone Page and Button reference + +
+ + This automation is designed to work with the sliders and buttons found on pages 4 and 5 + + | Pages 4-5 | + |-----------| + | ![Pages 4-5](https://github.com/aderusha/HASwitchPlate/raw/dev/Documentation/Images/NextionUI_p4-p5_3sliders.png) | + +
+ + domain: automation + input: + haspdevice: + name: "HASPone Device" + description: "Select the HASPone device" + selector: + device: + integration: mqtt + manufacturer: "HASwitchPlate" + model: "HASPone v1.0.0" + hasppage: + name: "HASPone Page" + description: "Select the HASPone page (4 or 5) for the fan speed control. Refer to the HASPone Page and Button reference above." + default: 4 + selector: + number: + min: 4 + max: 5 + mode: slider + unit_of_measurement: page + haspbutton: + name: "HASPone Button" + description: "Select the HASPone button (7-9) for the fan speed control. Refer to the HASPone Page and Button reference above." + default: 7 + selector: + number: + min: 7 + max: 9 + mode: slider + unit_of_measurement: button + fan: + name: "Fan to control" + description: "Select a fan entity which supports percentage speed control" + selector: + entity: + domain: fan + text_on: + name: "HASPone Button Text On" + description: "Enter text to appear on the button above the fan speed control when the selected device is ON." + default: "Fan" + selector: + text: + text_off: + name: "HASPone Button Text Off" + description: "Enter text to appear on the button when the selected device is OFF. The default value of {{text_on}} will leave the text unchanged when the device turns on/off" + default: "{{text_on}}" + selector: + text: + font_select: + name: "HASPone Button Font" + description: "Select the text font for this button label. Refer to the HASPone Font reference above." + default: "6 - Noto Sans 32" + selector: + select: + options: + - "0 - Consolas 24" + - "1 - Consolas 32" + - "2 - Consolas 48" + - "3 - Consolas 80" + - "4 - Webdings 56" + - "5 - Noto Sans 24" + - "6 - Noto Sans 32" + - "7 - Noto Sans 48" + - "8 - Noto Sans 64" + - "9 - Noto Sans 80" + - "10 - Noto Sans Bold 80" + xcen_select: + name: "Text horizontal alignment" + description: "Horizontal text alignment: 0=Left 1=Center 2=Right" + default: "1 - Centered" + selector: + select: + options: + - "0 - Left aligned" + - "1 - Centered" + - "2 - Right aligned" + ycen_select: + name: "Text vertical alignment" + description: "Vertical text alignment: 0=Top 1=Center 2=Bottom" + default: "0 - Top aligned" + selector: + select: + options: + - "0 - Top aligned" + - "1 - Centered" + - "2 - Bottom aligned" + wrap: + name: "Text wrap" + description: "Enable line-wrapping text if too long to fit in the button." + default: false + selector: + boolean: + icon_on: + name: '"On" state icon' + description: 'Enter the icon to be shown when the selected entity is "on"' + default: "" + selector: + text: + icon_off: + name: '"Off" state icon' + description: 'Enter the icon to be shown when the selected entity is "off"' + default: "" + selector: + text: + +mode: parallel +max_exceeded: silent + +variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + hasppage: !input hasppage + haspbutton: !input haspbutton + fan: !input fan + text_on: !input text_on + text_off: !input text_off + font_select: !input font_select + font: '{{ font_select.split(" - ")[0] | int(default=6) }}' + xcen_select: !input xcen_select + xcen: '{{ xcen_select.split(" - ")[0] | int(default=1) }}' + ycen_select: !input ycen_select + ycen: '{{ ycen_select.split(" - ")[0] | int(default=1) }}' + wrap: !input wrap + icon_on: !input icon_on + icon_off: !input icon_off + fanbutton: "{{haspbutton|int}}" + togglebutton: "{{fanbutton-3}}" + fanobject: '{{ "p[" ~ hasppage ~ "].b[" ~ haspbutton ~ "]" }}' + toggleobject: '{{ "p[" ~ hasppage ~ "].b[" ~ togglebutton ~ "]" }}' + commandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ fanobject }}' + togglecommandtopic: '{{ "hasp/" ~ haspname ~ "/command/" ~ toggleobject }}' + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + jsoncommandtopic: '{{ "hasp/" ~ haspname ~ "/command/json" }}' + isbr: "{% if wrap == true %}1{% else %}0{% endif %}" + activepage: >- + {%- set activepage = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^number\..*_active_page(?:_\d+|)$") -%} + {%- set activepage.entity=entity -%} + {%- endif -%} + {%- endfor -%} + {{ states(activepage.entity) | int(default=-1) }} + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + selectedfg: >- + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + selectedbg: >- + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_selected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + unselectedfg: >- + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_foreground_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + unselectedbg: >- + {%- set color = namespace() -%} + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^light\..*_unselected_background_color(?:_\d+|)$") -%} + {%- set color.source=entity -%} + {%- endif -%} + {%- endfor -%} + {%- set brightness = state_attr(color.source, "brightness")|int(default=255) / 255 -%} + {%- set red=(state_attr(color.source, "rgb_color")[0] * brightness)|int(default=0) -%} + {%- set green=(state_attr(color.source, "rgb_color")[1] * brightness)|int(default=0) -%} + {%- set blue=(state_attr(color.source, "rgb_color")[2] * brightness)|int(default=0) -%} + {{ (red|bitwise_and(248)*256) + (green|bitwise_and(252)*8) + (blue|bitwise_and(248)/8)|int }} + + ypos: "{{(haspbutton|int - 7) * 90 + 32}}" # calculate the top pixel position based on the selected button + xpos: 0 + icon: '{% if states(fan) == "on" %}{{icon_on}}{% else %}{{icon_off}}{% endif %}' + text: '{% if states(fan) == "on" %}{{text_on}}{% else %}{{text_off}}{% endif %}' + iconwidth: 60 + iconheight: 65 + iconfont: 8 + percentage: '{{state_attr(fan, "percentage")|int(default=0)}}' + fanvalue: "{{((percentage|int(default=0)) * 255 / 100)|round(0)|int}}" + +trigger_variables: + haspdevice: !input haspdevice + haspname: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{- entity|regex_replace(find="^sensor\.", replace="", ignorecase=true)|regex_replace(find="_sensor(?:_\d+|)$", replace="", ignorecase=true) -}} + {%- endif -%} + {%- endfor -%} + haspsensor: >- + {%- for entity in device_entities(haspdevice) -%} + {%- if entity|regex_search("^sensor\..+_sensor(?:_\d+|)$") -%} + {{ entity }} + {%- endif -%} + {%- endfor -%} + jsontopic: '{{ "hasp/" ~ haspname ~ "/state/json" }}' + selectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedforegroundcolor/rgb" }}' + selectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/selectedbackgroundcolor/rgb" }}' + unselectedfgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedforegroundcolor/rgb" }}' + unselectedbgtopic: '{{ "hasp/" ~ haspname ~ "/light/unselectedbackgroundcolor/rgb" }}' + +triggers: + - trigger: state + entity_id: !input fan + - trigger: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + - trigger: homeassistant + event: start + - trigger: mqtt + topic: "{{jsontopic}}" + - trigger: mqtt + topic: "{{selectedfgtopic}}" + - trigger: mqtt + topic: "{{selectedbgtopic}}" + - trigger: mqtt + topic: "{{unselectedfgtopic}}" + - trigger: mqtt + topic: "{{unselectedbgtopic}}" + +condition: + - condition: template + value_template: "{{ is_state(haspsensor, 'ON') }}" + +action: + - choose: + ######################################################################### + # RUN ACTIONS or Home Assistant Startup or HASPone Connect + # Apply styles, place text, and then place icon if our target page is currently active + - conditions: + - condition: template + value_template: >- + {{- + (trigger is not defined) + or + (trigger.platform is none) + or + ((trigger.platform == 'homeassistant') and (trigger.event == 'start')) + or + ((trigger.platform == 'template') and (trigger.entity_id == haspsensor) and (trigger.to_state.state == 'ON')) + -}} + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: >- + ["{{toggleobject}}.txt=\"{{text}}\"", + "{{toggleobject}}.font={{font}}", + "{{toggleobject}}.xcen={{xcen}}", + "{{toggleobject}}.ycen={{ycen}}", + "{{toggleobject}}.pco={{selectedfg}}", + "{{toggleobject}}.bco={{selectedbg}}", + "{{toggleobject}}.pco2={{unselectedfg}}", + "{{toggleobject}}.bco2={{unselectedbg}}", + "{{fanobject}}.pco={{unselectedfg}}", + "{{fanobject}}.bco={{unselectedbg}}", + "{{fanobject}}.val={{fanvalue}}" + {%- if activepage|int == hasppage|int -%} + ,"delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","vis {{fanbutton}},1" + {%- endif -%}] + + ######################################################################### + # Update display if our entity has changed state + - conditions: # fan has changed value, on/off state has also changed, and we're currently on the selected page + - condition: template + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == fan) and (trigger.from_state.state != trigger.to_state.state) and (activepage|int == hasppage|int) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["{{toggleobject}}.txt=\"{{text}}\"","{{fanobject}}.val={{fanvalue}}","delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\""]' + - conditions: # fan has changed value, on/off state has also changed + - condition: template + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == fan) and (trigger.from_state.state != trigger.to_state.state) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["{{toggleobject}}.txt=\"{{text}}\"","{{fanobject}}.val={{fanvalue}}"]' + - conditions: # fan speed percentage has changed + - condition: template + value_template: '{{ (trigger.platform == "state") and (trigger.entity_id == fan) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["{{fanobject}}.val={{fanvalue}}"]' + + ######################################################################### + # Handle MQTT message triggers + - conditions: + - condition: template + value_template: '{{ trigger.platform == "mqtt" }}' + sequence: + - choose: + ######################################################################### + # Catch incoming JSON messages + - conditions: + - condition: template + value_template: "{{ (trigger.topic == jsontopic) and trigger.payload_json is defined }}" + sequence: + - choose: + ######################################################################### + # Primary function: Toggle a fan when we press the button + - conditions: + - condition: template + value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == toggleobject) and (trigger.payload_json.value == "OFF") }}' + sequence: + - service: fan.toggle + entity_id: !input fan + ######################################################################### + # Primary function: Set the fan speed percentage when the HASPone slider has moved + - conditions: + - condition: template + value_template: '{{ (trigger.topic == jsontopic) and (trigger.payload_json.event == fanobject ~ ".val") }}' + sequence: + - choose: + - conditions: + - condition: template + value_template: '{{ trigger.payload_json.value | int(default=0) == 0 }}' + sequence: + - service: fan.turn_off + entity_id: !input fan + default: + - service: fan.set_percentage + entity_id: !input fan + data: + percentage: >- + {%- set value = trigger.payload_json.value | int(default=0) -%} + {%- if value >= 250 -%} + 100 + {%- else -%} + {{ ((value * 100 / 255)|round(0, 'ceil')|int) }} + {%- endif -%} + ######################################################################### + # Icon overlay + - conditions: # Page changed to our page, so place the icon on the screen. + - condition: template + value_template: '{{ (trigger.topic == jsontopic ) and (trigger.payload_json.event == "page" ) and (trigger.payload_json.value == hasppage|int) }}' + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["ref {{togglebutton}}","delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{fanbutton}},1"]' + + ######################################################################### + # Theme: Apply selected foreground color when it changes. + # Any change to the button will remove the overlaid icon. + - conditions: + - condition: template + value_template: "{{ trigger.topic == selectedfgtopic }}" + sequence: + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{togglecommandtopic}}.pco" + payload: "{{trigger.payload}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{trigger.payload}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{fanbutton}},1"]' + ######################################################################### + # Theme: Apply selected background color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == selectedbgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{togglecommandtopic}}.bco" + payload: "{{trigger.payload}}" + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{fanbutton}},1"]' + ######################################################################### + # Theme: Apply unselected foreground color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == unselectedfgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["{{toggleobject}}.pco2={{trigger.payload}}","{{fanobject}}.pco={{trigger.payload}}"]' + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{fanbutton}},1"]' + ######################################################################### + # Theme: Apply unselected background color on change + - conditions: + - condition: template + value_template: "{{ trigger.topic == unselectedbgtopic }}" + sequence: + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["{{toggleobject}}.bco2={{trigger.payload}}","{{fanobject}}.bco={{trigger.payload}}"]' + - condition: template + value_template: "{{ activepage|int == hasppage|int }}" + - delay: "00:00:00.5" + - service: mqtt.publish + data: + topic: "{{jsoncommandtopic}}" + payload: '["delay=1","xstr {{xpos}},{{ypos}},{{iconwidth}},{{iconheight}},{{iconfont}},{{selectedfg}},0,1,1,3,\"{{icon}}\"","delay=1","vis {{fanbutton}},1"]'