HASwitchPlate / openHASP

HomeAutomation Switchplate based on lvgl for ESP32
https://www.openhasp.com
MIT License
713 stars 184 forks source link

Feature request: data tags for objects #195

Closed nagyrobi closed 2 years ago

nagyrobi commented 3 years ago

Is your feature request related to a problem? Please describe. Trying to create an interactive panel with popups to control various advanced properties for multiple entities shown on the page.

For example, cover control has 3 buttons: UP, STOP and DOWN. Now if you have 3 windows in a room you'll have 3 covers that's 9 buttons - that's still OK.

I'd like to include a few shortcut position buttons for these covers, like 1/3, 3/4 and "small holes", and a slider for free positioning. Now having all these for all 3 covers on the same page would not only very heavy but also hard to control. Since these properties are used more rarely, I'd debloat the screen by generating dynamically an obj from HA with the buttons and the slider popping up on long-pressing the stop button on the page.

Now the problem is that I can do that effectively only for a single cover with the CC. To do it for each cover I'd have to duplicate the entire config of the popups using new ids for each cover. Painful especially because these are only drawn for a short while and then deleted (could also be shown/hidden)

Describe the solution you'd like Have an universal tag property (re-writable) for all objects, supporting strings up to, say 100 characters. If tag is set ( != empty string) for an object, always return it together with the event.

If this would be possible, I would only have to create the objects uniquely with the tags set to the entity_id of the objects in HA, and processing the events of these objects later can be done universally!

Additional context

example

# this is a STOP button for COVER1 on the main page, long-press for the popup
      - obj: "p1b6"  
        properties:
          "text_color": "{{ '#FFFF00' if is_state('cover.my_room_1', 'closing') else '#FFFFFF' }}"
          "text_opa": "{{ '80' if is_state_attr('cover.my_room_1','current_position', 0) else '255' }}"
        event:
          "down":
            - service: cover.close_cover
              target:
                entity_id: "cover.my_room_1"
          "hold":
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: jsonl
                parameters: >-
                  {"page":1,"id":200,"obj":"obj","x":5,"y":55,"w":230,"h":190,"shadow_opa":255,"shadow_color":"black","shadow_width":20,"shadow_spread":0}
                  {"page":1,"id":201,"obj":"btn","parentid":200,"x":10,"y":40,"w":150,"h":60,"toggle":false,"text_font":24,"text":"Small holes","tag":"cover.my_room_1"}
                  {"page":1,"id":202,"obj":"btn","parentid":200,"x":10,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"1/3","tag":"cover.my_room_1"}
                  {"page":1,"id":203,"obj":"btn","parentid":200,"x":90,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"3/4","tag":"cover.my_room_1"}
                  {"page":1,"id":204,"obj":"slider","parentid":200,"x":180,"y":40,"w":30,"h":130,"min":1,"max":100,"tag":"cover.my_room_1","val":
                  {{- state_attr('cover.my_room_1','current_position') -}}
                  }
                  {"page":1,"id":205,"obj":"label","parentid":200,"x":10,"y":10,"w":210,"h":20,"text":"Quick positions for COVER 1","align":"center"}

# this is a STOP button for COVER2 on the main page, long-press for the popup
      - obj: "p1b6"  
        properties:
          "text_color": "{{ '#FFFF00' if is_state('cover.my_room_2', 'closing') else '#FFFFFF' }}"
          "text_opa": "{{ '80' if is_state_attr('cover.my_room_2','current_position', 0) else '255' }}"
        event:
          "down":
            - service: cover.close_cover
              target:
                entity_id: "cover.my_room_2"
          "hold":
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: jsonl
                parameters: >-
                  {"page":1,"id":200,"obj":"obj","x":5,"y":55,"w":230,"h":190,"shadow_opa":255,"shadow_color":"black","shadow_width":20,"shadow_spread":0}
                  {"page":1,"id":201,"obj":"btn","parentid":200,"x":10,"y":40,"w":150,"h":60,"toggle":false,"text_font":24,"text":"Small holes","tag":"cover.my_room_2"}
                  {"page":1,"id":202,"obj":"btn","parentid":200,"x":10,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"1/3","tag":"cover.my_room_2"}
                  {"page":1,"id":203,"obj":"btn","parentid":200,"x":90,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"3/4","tag":"cover.my_room_2"}
                  {"page":1,"id":204,"obj":"slider","parentid":200,"x":180,"y":40,"w":30,"h":130,"min":1,"max":100,"tag":"cover.my_room_2","val":
                  {{- state_attr('cover.my_room_2','current_position') -}}
                  }
                  {"page":1,"id":205,"obj":"label","parentid":200,"x":10,"y":10,"w":210,"h":20,"text":"Quick positions for COVER 2","align":"center"}

# this is a STOP button for COVER3 on the main page, long-press for the popup
      - obj: "p1b6"  
        properties:
          "text_color": "{{ '#FFFF00' if is_state('cover.my_room_1', 'closing') else '#FFFFFF' }}"
          "text_opa": "{{ '80' if is_state_attr('cover.my_room_1','current_position', 0) else '255' }}"
        event:
          "down":
            - service: cover.close_cover
              target:
                entity_id: "cover.my_room_3"
          "hold":
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: jsonl
                parameters: >-
                  {"page":1,"id":200,"obj":"obj","x":5,"y":55,"w":230,"h":190,"shadow_opa":255,"shadow_color":"black","shadow_width":20,"shadow_spread":0}
                  {"page":1,"id":201,"obj":"btn","parentid":200,"x":10,"y":40,"w":150,"h":60,"toggle":false,"text_font":24,"text":"Small holes","tag":"cover.my_room_3"}
                  {"page":1,"id":202,"obj":"btn","parentid":200,"x":10,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"1/3","tag":"cover.my_room_3"}
                  {"page":1,"id":203,"obj":"btn","parentid":200,"x":90,"y":110,"w":70,"h":60,"toggle":false,"text_font":24,"text":"3/4","tag":"cover.my_room_3"}
                  {"page":1,"id":204,"obj":"slider","parentid":200,"x":180,"y":40,"w":30,"h":130,"min":1,"max":100,"tag":"cover.my_room_3","val":
                  {{- state_attr('cover.my_room_3,'current_position') -}}
                  }
                  {"page":1,"id":205,"obj":"label","parentid":200,"x":10,"y":10,"w":210,"h":20,"text":"Quick positions for COVER 3","align":"center"}

# below the processor objects for the cover
      - obj: "p1b201" # Small holes
        event:
          "down":
            - service: cover.set_cover_position
              data:
                entity_id: "{{ tag }}"
                position: 19
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: p1b200.delete

      - obj: "p1b202" # 1/3
        event:
          "down":
            - service: cover.set_cover_position
              data:
                entity_id: "{{ tag }}"
                position: 48
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: p1b200.delete

      - obj: "p1b203" # 3/4
        event:
          "down":
            - service: cover.set_cover_position
              data:
                entity_id: "{{ tag }}"
                position: 90
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: p1b200.delete

      - obj: "p1b204" # slider
        event:
          "up":
            - service: cover.set_cover_position
              data:
                entity_id: "{{ tag }}"
                position: "{{ val | int }}"
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: p1b200.delete

      - obj: "p1b201" # Just close popup
        event:
          "down":
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_4
              data:
                keyword: p1b200.delete
nagyrobi commented 3 years ago

Don't know if it's worth considering multiple tags. Like tag1, tag2, ... tagN...

Edit: if it would support enough string length, I think one tag would suffice. Multiple 'sub'-tags could be used perhaps by placing a json string or a dict within the string.

fvanroie commented 3 years ago

I'm not sure about adding arbitrary attribute data, as this is going outside of the lvgl object model...

nagyrobi commented 3 years ago

Would be very useful when integration with HA becomes more and more complex.

nagyrobi commented 3 years ago

Another interesting note is that I have 5 plates operating on the same HA instance, with custom component. Each plate in a different room, showing 5-8 pages depending on location. 4 of them have weather, all of them have climate control + various switches, 3 of them have media player, some of them have convers control (total 10 covers, and don't have all of them in OpenHASP yet).

Load of the custom component with this config in HA takes 25.62 seconds (most components load in less than 1 second, some in less than 5 seconds, and only a few take more than that). I have only implemented the above for one single cover but If I do it for all of them, that will increase boot time significantly...

That's why I think that a way to "functionalize" certain routines would not only make config easier/smaller but also would speed up operation/boot. Think of re-syncing of the objects on reconnects etc...

I know that a number of 5 plates is not very usual, but also not very big - also not maxing out the pages count of them. I don't think this is too much.

Side note, my HA instance is running in a VM on a HP DL380 server with 16 Xeon cores and 128GB RAM... I don't ever dare to think what would it look like on a raspberry-kind of hardware...

fvanroie commented 3 years ago

It would be interesting to know why the components takes this long. It seems that there is a synchronous call, delay or time-out... I don't know.

dgomes commented 3 years ago

I think it's due to the amount of mqtt messages being sent (1 per line)

A recent commit will provide a better performance

kquinsland commented 2 years ago

I think it's a bit of a hack/workaround, but what about moving most of the logic around controlling the covers into a script.

Then, you have something like this:

      - obj: "p1b6"  
        properties:
          "text_color": "{{ '#FFFF00' if is_state('cover.my_room_1', 'closing') else '#FFFFFF' }}"
          "text_opa": "{{ '80' if is_state_attr('cover.my_room_1','current_position', 0) else '255' }}"
        event:
          "down":
            - service: script.position_all_covers
              data:
                oHaspEvent: "down"

Then the position_all_covers script is called with the argument "down". In the script you would look up the position(s) of each cover and then tell HA to adjust it's state +/- whatever a down event should increment by.

nagyrobi commented 2 years ago

The goal is not to position all covers, just one. You'd have the same quick position control, usable separately for each one.

kquinsland commented 2 years ago

The goal is not to position all covers, just one. You'd have the same quick position control, usable separately for each one.

I think i misunderstood? You want to dynamically create a few 'bookmarks' for fixed positions for a single cover? You want to have a single set of the bookmarks but be able to choose which cover the bookmarks are for?

It's another hack but perhaps a dropdown listing all the covers. When you select a cover, the CC will adjust the 'selected' value of some input_select. When a bookmark button is pressed, look up the current value of the input_select and use that to derive the entity ID that you feed into the entity_id for cover.close...

fvanroie commented 2 years ago

I guess the event can be appended with a tag... The data format depends on the schema validation in the CC The tag could be a fixed tag key in the event with custom payload or custom key + value ...

nagyrobi commented 2 years ago

+1 for fixed tag with payload as string, max 100 chars. One could put in a string whatever, json or alike.

kquinsland commented 2 years ago

The data format depends on the schema validation in the CC The tag could be a fixed tag key in the event with custom payload or custom key + value ...

it is trivial to disable validation and keep it treated as a 'raw' string rather than try to parse it as json/int...etc.

I keep going back and forth on weather or not it should be a small JSON object as this can make parsing inside of HA/Jinja2 templates a lot easier or just a freeform string which would require almost no changes to the CC but would increase the complexity of each HA/J2 template.

fvanroie commented 2 years ago

In the nightly build you can find that all objects have received a tag property. It can contain custom JSON (or number or text) that will be passed along with each event.

nagyrobi commented 2 years ago

The principle described in the OP works as long as the tags are short.

MQTT payload is truncated:

[15:06:20.239][65524/98224 33][18980/19180  2] MQTT PUB: p1b201 => {"event":"down","tag":"cover.nappali_sz
[15:06:20.352][65524/97984 33][17296/19172 10] MQTT PUB: p1b201 => {"event":"up","tag":"cover.nappali_szom

also

2022-01-27 21:06:20 ERROR (MainThread) [custom_components.openhasp] Error decoding received JSON message: {"event":"down","tag":"cover.nappali_sz on hasp/plate_nappali/state/p2b201
2022-01-27 21:06:20 ERROR (MainThread) [custom_components.openhasp] Error decoding received JSON message: {"event":"up","tag":"cover.nappali_szom on hasp/plate_nappali/state/p2b201
fvanroie commented 2 years ago

The principle described in the OP works as long as the tags are short.

Thanks for the report @nagyrobi !

I found a few instances where the event buffer was indeed very short (40 or 100 characters total). It explains the 39 character limit you encountered for generic objects. This has now been adjusted to 512 characters for the whole event message (including the tag) across all event handlers.

I think that's a reasonable amount. Maybe we need to limit the tag length, so it will always fit. But for now, go ahead and test again. Any tag upto 400 characters should be fine.

NB: you can set the tag to a JSON object like p1b7.tag={"mykey":"myvalue","test":true}

MQTT PUB: p1b7 => {"event":"down","val":1,"tag":{"mykey":"myvalue","test":true}}
MQTT PUB: p1b7 => {"event":"up","val":0,"tag":{"mykey":"myvalue","test":true}}
nagyrobi commented 2 years ago

Thank you very much I think this will be enough for most use cases. One suggestion: Missing tags could be skipped from the payloads: [06:30:13.507][65524/98096 33][16844/19188 13] MQTT PUB: p2b200 => {"event":"down","tag":null} [06:30:23.964][65524/97868 33][18836/19076 2] MQTT PUB: p2b8 => {"event":"release","tag":null} I think it's enough to only send the tag key when it contains data.

One could test against a valid tag key with a template like this:

{% if value_json.tag is defined %}
Tag defined
{%- endif %}
nagyrobi commented 2 years ago

NB: you can set the tag to a JSON object like p1b7.tag={"mykey":"myvalue","test":true}

Tested with tags containing JSON objects, it's perfectly compatible with the CC out of the box (@dgomes):

      - obj: "p2b52"
        event:
          "hold":
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_teszt
              data:
                keyword: jsonl
                parameters: >-
                  {"page":2,"id":200,"obj":"obj","x":5,"y":55,"w":230,"h":190,"shadow_opa":255,"shadow_color":"black","shadow_width":20,"shadow_spread":0}
                  {"page":2,"id":201,"obj":"btn","parentid":200,"x":10,"y":40,"w":150,"h":60,"toggle":false,"text_font":24,"text":"Lyukacsos","tag":{"cover":"cover.konyha_v","position":"19"}}

      - obj: "p2b201" # redőny gyorspopup Lyukacsos
        event:
          "down":
            - service: cover.set_cover_position
              data:
                entity_id: "{{ tag.cover }}"
                position: "{{ tag.position }}"
            - service: openhasp.command
              target:
                entity_id: openhasp.plate_teszt
              data:
                keyword: p2b200.delete

This is ready to be documented ;) I'm running it in production.

nagyrobi commented 2 years ago

Made 2 examples for documentation:

kép press color label > kép

kép long-press Stop > kép

kquinsland commented 2 years ago

Ohh i really like the light toggle/color/long-press... that is a lot smarter than the interface that i was designing..! please open a PR against the docs repo so I can borrow that implementation :D

nagyrobi commented 2 years ago

As soon as @fvanroie decides to make a new dev brach on the docs repo, as this is not (yet) in the release.

fvanroie commented 2 years ago

There is a 0.6.3-dev docs branch. tag can go in 0.6.3 release.

nagyrobi commented 2 years ago

Done: https://haswitchplate.github.io/openHASP-docs/0.6.3/integrations/home-assistant/sampl_conf/#using-tags