Add last_triggered_by Attribute to states

Ok this worked!

This automation will only write some data (time, user and user_id) when the values where changed by a person, I write it as Json, so I can use it later:

alias: Track Airco Bureau HVAC Mode or Temperature Change with JSON
description: >-
  Tracks the last user and time when the HVAC mode or temperature is changed on
  airco_bureau.
triggers:
  - entity_id: climate.e8fb1cffxxx
    attribute: hvac_mode
    trigger: state
  - entity_id: climate.e8fb1cffxxx
    attribute: temperature
    trigger: state
conditions:
  - condition: template
    value_template: >-
      {{ trigger.to_state.context.user_id is not none and trigger.to_state.context.user_id | string | length > 28 }}
actions:
  - target:
      entity_id: input_text.airco_bureau_last_change_json
    data:
      value: >-
        {% set user_id = trigger.to_state.context.user_id %}  
        {% set user_name = (states.person | selectattr('attributes.user_id', '==', user_id) | map(attribute='name') | list | first) or "system" %}  
       {{
          {
            "user": user_name if user_name else user_id,
            "time": now().isoformat(),
            "user_id": user_id
          } | tojson
        }}
    action: input_text.set_value
mode: single

This will write to an text helper: input_text.airco_bureau_last_change_json

{'time': '2024-10-27T14:09:49.195004+01:00', 'user': 'The Uname', 'user_id': '42041fw2967a49xxx4fe2bf2610b01d1'}

You can use it in template or script like this example; last_changed will read the data and convert the json string to an object, then I convert the datetime string to date object, get the start time (6 in the morning) and enddate(6 in the evening) and check if its between those dates and just for example print the data of the object one by one:

{% set last_changed = states('input_text.airco_bureau_last_change_json') | from_json %}
{% set my_datetime = strptime(last_changed.time , '%Y-%m-%dT%H:%M:%S.%f%z') %}
{% set start_time = now().replace(hour=6, minute=0, second=0, microsecond=0) %}
{% set end_time = now().replace(hour=18, minute=0, second=0, microsecond=0) %}
{% set start_time = start_time + (my_datetime.utcoffset() if my_datetime.utcoffset() else timedelta(0)) %}
{% set end_time = end_time + (my_datetime.utcoffset() if my_datetime.utcoffset() else timedelta(0)) %}
{{ my_datetime >= start_time and my_datetime <= end_time }}
{{ last_changed.user }}
{{ last_changed.user_id }}
{{ last_changed.time }}

output:

True
The Uname
42041fw2967a49xxx4fe2bf2610b01d1
2024-10-27T14:09:49.195004+01:00

Or simply check the date of the state, its the same and then you don’t need to do all the conversion of string to date

Why aren’t you making this a template sensor? You’re just duplicating information for essentially no reason into a input helper which anyone can alter. And you’re locked in to at-most 254 characters.

template:
  trigger:
  - entity_id: climate.e8fb1cffxxx
    attribute: hvac_mode
    trigger: state
  - entity_id: climate.e8fb1cffxxx
    attribute: temperature
    trigger: state
  condition:
  - condition: template
    value_template: >-
      {{ trigger.to_state.context.user_id is not none and trigger.to_state.context.user_id | string | length > 28 }}
  action:
  - variables:
      user_id: "{{ trigger.to_state.context.user_id }}"
      user_name:  "{{ (states.person | selectattr('attributes.user_id', '==', user_id) | map(attribute='name') | list | first) or 'system' }}"
  sensor:
  - name: airco_bureau_last_change
    unique_id: airco_bureau_last_change
    state: "{{ user_name }}"
    attributes:
      user_id: "{{ user_id }}"
      # Only add this if you want time to survive restarts
      last_changed: "{{ now() }}"

Then you can simply put this anywhere in the UI, and it will act like a normal sensor. No need to store the time unless you want it to survive restarts.

EDIT: Fixed spacing to make it work out of the box.

3 Likes

@HSken “This is the way.”

All of the above looks terrific and I was going to mention this:

Also with the context information, in my wanderings I have read that it is not entirely reliable, but I believe those statements probably had to do with the fact those pieces of information are so volatile, sometimes changing before people can refer back to them when they need to (hence my need for template sensors but I had no need to have my data survive restarts).

If you wanted to have them survive restarts but still only refer to the template sensor in your code, then you could have HA Startup and Shutown triggered automations that would read and write the input helper as well but that seems like overill as it is just as easy to refer to the input helper in code anyway lol

1 Like

Good idea,I’m new to all this, didn’t know you could fill a new sensor like that :blush: , thanks will give that a try also. Is indeed a more elegant way.

But not really worried about this storage, its running on my home lab server, think this VM has over 60GB of storage and can increase it any time,

I have some senors creating a log every 10sec thats probably much bigger issue!

Ow missed you did a template and not an automation. :roll_eyes: :man_facepalming: :blush:
Did some more digging on that:

There must have been some error in your example @petro, but I got it working :partying_face: :

template:
  - trigger:
      - platform: state
        entity_id: climate.e8fb1cffxxx
        attribute: hvac_mode
      - platform: state
        entity_id: climate.e8fb1cffxxx
        attribute: temperature
    condition:
      - condition: template
        value_template: >-
          {{ trigger.to_state.context.user_id is not none and
 trigger.to_state.context.user_id | string | length > 28 }}
    sensor:
      - name: "Airco Bureau Last Changed By"
        unique_id: airco_bureau_last_changed_by
        state: >-
          {% set user_id = trigger.to_state.context.user_id %}
          {% set user_name = states.person | 
selectattr('attributes.user_id', '==', user_id) 
| map(attribute='name') | list | first %}
          {{ user_name or 'system' }}
        attributes:
          user_id: "{{ user_id }}"
          last_changed: "{{ now() }}"

Thanks a lot @KruseLuds and @petro think you both gave me the solution before I acture figured it out by my self, but I like to understand what I’m doing, also hope this my help your case @KruseLuds.
I want to point out once more to you that I had lots of issues with the user_id is not none and found the user_id | string | length > 28 much more reliable.

And without that help I would probably never figured out I could use a sensor for that.

1 Like

Yes idd, thats what i noticed also, because in my case the state attributes contain so much data, one of them the room temprature with even a precision of one decimal, it get changed all the time, but I don’t care about those changes, so I think this is the way to go; you check only the values that you know people (can) change or where you care about that they changed and save them in a sensor; like @petro suggested.

Also note the: trigger.to_state.context.user_id | string | length > 28 in the condition, took me very long, for some reason the trigger.to_state.context.user_id is not none wasn’t reliable working on me, but as the id’s are uuids they are normaly over 30 long … took 28 just to be safe.

Interesting I’ve never noticed that -

and yet you mark your own post as the solution…

It is common courtesy in this community to not do that, and mark the post that gave you all you need as solution

we’re all glad you got it working for your needs, so give credits where credits are due please

1 Like

It doesn’t make sense. the test none checks for the none type object, that’s it. If they are having problems with it, they likely were making a mistake somewhere. For all intents and purposes, user_id’s are None or a string, they don’t reference any other strings so is not none will be the most reliable.

EDIT: I now see why they were likily having issues, they are casting the value to a string and probably made a mistake at one point checking for none with the | string in place.

As for the code in home assistant, the user_id can only be a str or None.

1 Like