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.
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
Good idea,I’m new to all this, didn’t know you could fill a new sensor like that , 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.
Did some more digging on that:
There must have been some error in your example @petro, but I got it working :
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.
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.
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.