My solution to this problem ended up being a custom template sensor sensor.person_data, an AppDaemon program, with a custom Lovelace card setup to make it all work.
Then, if I want to get any data out, I just pull it out of sensor.person_data.
Template Sensor (configuration.yaml):
template:
# Person Data Storage
- trigger:
- platform: event
event_type: set_person_data
- platform: event
event_type: remove_person_data
sensor:
- unique_id: 474f308c-61fc-4732-9991-187893762636
name: Person Data
state: Person Data
attributes:
data: >
{% set current = this.attributes.get('data',{}) %}
{% set person_dict = current.get(trigger.event.data.person,{}) %}
{% if trigger.event.event_type == 'set_person_data' %}
{% set new = {trigger.event.data.key: trigger.event.data.value} %}
{% set person_dict = {trigger.event.data.person: dict(person_dict, **new)} %}
{% elif trigger.event.event_type == 'remove_person_data' %}
{% set person_dict = {trigger.event.data.person: dict(person_dict.items() | rejectattr('0', 'eq', trigger.event.data.key))} %}
{% endif %}
{{ dict(current, **person_dict) }}
AppDaemon:
import appdaemon.plugins.hass.hassapi as hass
from datetime import datetime
import yaml
import appdaemon.utils as utils
'''
App which manages the person attribute data store.
Home Assistant has no mechanism to programatically manage attributes for person
entities. Instead, any custom attributes have to be managed entirely through
customize.yaml, and any changes made to those attributes outside of that file
will be lost when Home Assistant restarts. As such, there is a need to have some
kind of datastore outside of the person entities to store this data.
This application provides this data store, as well as the necessary hooks to make
it user-friendly to edit this data.
One challenge is that, if you use any entities stored in this data store in a
template sensor, the template sensor, by default, will only update when explicitly
referenced entities change. The entities referred to indirectly through the
data will not trigger the update. In order to enable the desired behavior, this
app will automatically update a timestamp any time any of its underlying entities
are updated, which should trigger the corresponding update in any templates.
A downside of this approach is that all template sensors that refer to any data
in this datastore will update when any of the data in the datastore is updated,
but depending on your use case, this will hopefully not be too many updates.
Variables of relevance:
`self.person_data` = the entity that stores the data. To ensure that this data
survives a reboot, it is recommended to create this data using a template
sensor.
`self.data_key` and `self.timestamp_key` = str, attribute names in self.person_data
`self.person_list_entity` = input_select helper for frontend
`self.person_attr_entity` = input_text helper for frontend
`self.person_value_entity` = input_text helper for frontend
`self.family_group` = The group that contains family members
Events:
This application registers the following events:
'person_data_write_int_value' causes the values in the interface helpers to be
written into the data store.
'person_data_del_int_value' causes the values in the interface helpers to be
deleted from the data store.
'''
class UpdatePersonAttributes(hass.Hass):
def initialize(self):
self.log("Initialize Person Attribute Updater")
#Variables
self.person_data = 'sensor.person_data'
self.data_key = 'data'
self.timestamp_key = 'update_time'
self.person_list_entity = 'input_select.person_attributes_person'
self.person_attr_entity = 'input_text.person_attributes_attribute'
self.person_value_entity = 'input_text.person_attributes_value'
self.family_group = 'group.family'
self.listener_handles = []
#Listen Hooks
self.listen_event(self.update_person_list, 'app_terminated')
self.listen_event(self.write_value, 'person_data_write_int_value')
self.listen_event(self.delete_value, 'person_data_del_int_value')
self.listen_state(self.update_interface, self.person_list_entity)
self.listen_state(self.update_interface, self.person_attr_entity)
#Initialize
self.update_person_list()
self.update_listeners()
def update_interface(self, *args, **kwargs):
''' Update interface based on selections '''
#Get Attributes
person = self.get_state(self.person_list_entity)
attribute = self.get_state(self.person_attr_entity)
try:
value = self.get_state(self.person_data, attribute=self.data_key)[person][attribute]
except KeyError:
value = 'unknown'
#Set Value
self.set_state(self.person_value_entity, state=value)
def write_value(self, *args, **kwargs):
''' Save the interface value into the data store '''
person = self.get_state(self.person_list_entity)
attribute = self.get_state(self.person_attr_entity)
value = self.get_state(self.person_value_entity)
full_state = self.get_state(self.person_data, attribute='all')
full_attributes = full_state['attributes']
full_attributes[self.data_key][person][attribute] = value
self.set_state(self.person_data, attributes=full_attributes)
self.update_listeners()
def delete_value(self, *args, **kwargs):
''' Delete the interface attribute from the data store '''
person = self.get_state(self.person_list_entity)
attribute = self.get_state(self.person_attr_entity)
full_state = self.get_state(self.person_data, attribute='all')
full_attributes = full_state['attributes']
full_attributes[self.data_key][person].pop(attribute,'unknown')
self.set_state(self.person_data, attributes=full_attributes)
self.update_listeners()
def update_person_list(self, *args, **kwargs):
''' Update input select helper with list of person entities '''
person_list = [p for p in self.get_state('person')]
self.call_service('input_select/set_options', entity_id=self.person_list_entity, options=person_list)
def update_listeners(self, *args, **kwargs):
''' Updates the listeners to include all of the different entities in the db '''
for handle in self.listener_handles:
self.cancel_listen_state(handle)
self.listener_handles.clear()
entities = []
data = self.get_state(self.person_data, attribute=self.data_key)
for person in data:
for _, entity in data[person].items():
if self.entity_exists(entity):
entities += [entity]
print(entities)
self.listener_handles = self.listen_state(self.update_timestamp, entities)
print(self.listener_handles)
self.update_timestamp()
def update_timestamp(self, *args, **kwargs):
''' Updates the timestamp '''
full_state = self.get_state(self.person_data, attribute='all')
full_attributes = full_state['attributes']
full_attributes[self.timestamp_key] = datetime.now().isoformat()
self.set_state(self.person_data, attributes=full_attributes)
Lovelace Card:
type: vertical-stack
cards:
- type: entities
title: Attributes Editor
entities:
- entity: input_select.person_attributes_person
- entity: input_text.person_attributes_attribute
- entity: input_text.person_attributes_value
- type: custom:paper-buttons-row
styles:
justify-content: flex-end
buttons:
- icon: mdi:delete
tap_action:
action: fire-event
event_type: person_data_del_int_value
- icon: mdi:floppy
tap_action:
action: fire-event
event_type: person_data_write_int_value
- type: custom:fold-entity-row
head:
type: section
label: Attributes
entities:
- type: custom:auto-entities
card:
type: entities
card_mod:
style: |
ha-card {
margin: 0px -16px;
}
filter:
template: >-
{%- set data = state_attr('sensor.person_data', 'data') -%} {%-
for
member in data -%} { 'entity': '{{member}}' }, {%- for key in
data[member] -%} {% set value = data[member][key] -%} {{ {
'type': 'custom:template-entity-row',
'name': key,
'state': value
} }}, {%- endfor -%}{%- endfor -%}
view_layout:
column: 3