So I had a bit of time to work on it, as I must agree that I like the idea of using HA as a state reporter and generally a middleware, but the granular control over domains are paramount. Since the HA team is also working on persistent entity changes I want to keep my code in a way that don’t incur breaking changes: like using hass.set_state()
should be considered safe from the AD perspective.
I recreated the entity_augment app so that you can supply a list of top level domains under either a whitelist or blacklist, whitelist is prioritized and listen to state changes rather than the state_changed
event. Additionally the boolean reflect paramter decided if values are reflected back to HA, defaults to true. Example apps.yaml
entry:
entity_augment:
module: entity_augment
class: EntityAugment
blacklist:
- sensor
- light
db_path: /conf/states_db.json
reflect: True
The app itself now looks like this:
import hassapi as hass
from tinydb import TinyDB, Query
"""
An AD app to create and manage data entities and their states and attributes, it also allows for overwriting (reflecting) HA entities (like new attributes) optionally new domains and entities can be dynamically created as well.
In case of using automations or the like that depends on or is an aggreagate of the event itself being fired, two optional parameters exist, with black and whitelisting where the latter takes precedence.
"""
class EntityAugment(hass.Hass):
"""
Creates the database is not present and initializes based on supplied parameters
"""
def initialize(self):
self.db = db = TinyDB(self.args['db_path'])
self.query = Query()
self.init_reflect()
self.callback_delegation()
"""
Checks if the reflect parameter and listens to appropriate events
"""
def init_reflect(self):
try:
self.reflect = self.args['reflect']
except KeyError:
self.reflect = True
if self.reflect:
self.listen_event(self.populate_entities, 'homeassistant_start')
"""
Checks if the whitelist or blacklist parameter is set and chooses a callback for the 'state_changed' event listener
"""
def callback_delegation(self):
try:
for item in self.args['whitelist']:
self.listen_state(self.wupdate, item, attribute='all')
except KeyError:
try:
self.list = self.args['blacklist']
self.listen_event(self.blacklist_update, 'state_changed')
except KeyError:
self.log('Tracking all entities')
self.listen_event(self.event_update, 'state_changed')
"""
This is a dynamic constraint on the entity domain, for now it only filters on top level domains (lights, sensor etc)
It is only used as a wrapper if any domain filters are active.
"""
def blacklist_update(self, event_name, data, kwargs):
if data['entity_id'].split('.')[0] not in self.list:
self.event_update(event_name, data, kwargs)
"""
Every time a state_changed event that fulfills our constraints is fired this function will update the database
- If the item is new, insert new entry into tinyDB
- If the attribute keys differ, the latest attributes are combined, updated and optionally reflected back into HA
"""
def event_update(self, event_name, data, kwargs):
id = data['entity_id']
event_state = data['new_state']['state']
event_attrib = data['new_state']['attributes']
self.entity_update(id, event_state, event_attrib)
def entity_update(self, id, event_state, event_attrib):
db_entity = self.db.search(self.query.entity_id == id)
if(db_entity):
db_attrib = db_entity[0]['attributes']
keys = self.new_keys(db_attrib, event_attrib, data.get('remove_attributes',[]))
attrib = { "attributes": self.update_entity_attributes(event_attrib, db_attrib, keys), "state": event_state }
self.db.update(attrib, self.query.entity_id == id)
if(event_state.get('siblings', [])):
self.update_siblings(event_entity.split('.')[:-1].join('.'))
if(self.reflect and len(set(attrib.keys() - set(event_attrib.keys()))) != 0):
self.set_state(id, state=data['new_state']['state'], attributes=attrib)
else:
self.db.insert({"entity_id": id, "attributes": event_attrib})
"""
Function to find the list of all unique attribute keys in both tinyDB and HA entity registry
"""
def new_keys(self, hass_attrib, db_attrib, remove_keys=[]):
db_keys = db_attrib.keys()
hass_keys = hass_attrib.keys()
return(set(db_keys) | set(hass_keys)) - set(remove_keys)
"""
If the 'sibling' flag has been set to true in the event, this function will update all entity attributes under the same domain
"""
def update_siblings(self, domain):
for sibling in self.get_state(domain, attribute='all'):
attrib = db.search(self.query.entity_id == sibling['entity_id'])
for key in removing:
sibling.pop(key)
self.db.update(sibling, self.query.entity_id == sibling['entity_id'])
"""
Uses the updated attribute keys to create a new entry, using the HA value if present.
"""
def update_entity_attributes(self, hass_attrib, db_attrib, keys):
attrib = {}
for key in keys:
if key in hass_attrib:
attrib[key] = hass_attrib[key]
else:
attrib[key] = db_attrib[key]
return attrib
"""
Only run if reflect parameter is set to true, it ensures that on a HA restart the entities will reflect all augmented attributes
"""
def populate_entities(self, event_name, data, kwargs):
for entity in self.db.all():
id = entity['entity_id']
hass_state = self.get_state(id, attribute='all')
add = set(entity['attributes'].keys()) - set(hass_state['attributes'].keys())
for key in add:
hass_state['attributes'][key] = entity['attributes']
self.set_state(id, state=hass_state['state'], attributes=hass_state['attributes'])
If our only usecase is novel entities then this strategy should let us use HA for all its neat integrations and components on our dynamically generated entities.