Interaction Attribute for Entites

This is my first appdaemon snippet bear with me.
It adds 2 custom attributes to entities like switches with the following name and content
interaction: physical, automation, ui. Type of interaction
user : user that interacted

Config is very simple: a list of entities.
Enjoy
switchint.py

#
# Switchint 
#
# V1.0
# Add an attribute to an entity related to a physical switch describing
# User interaction type: physocal, automation, UI
# User that interacted
# based on
# ID combos: https://community.home-assistant.io/t/work-with-triggered-by-in-automations/400352/8
#interaction	id	      parent_id	    user_id
#Physical	    Not Null	Null	        Null
#Automation	  Not Null	Not Null	    Null
#UI	          Not Null	Null	        Not Null
#
# Sample config section for apps.yaml:
#switchint:
#  module: switchint
#  class: switchint
#  switches:
#    - light.light1
#    - light.light2
#    - light.light3
#    - light.light4

# TODO: better input check on entities, translate userid to friendly name of user


import hassapi as hass
from datetime import datetime, timedelta


class switchint(hass.Hass):
  def initialize(self):
    try:
      self.switches = list(self.args["switches"])
      self.loglevel = self.args.get("loglevel")
    except:
      self.mylog("Error opening parameters", INFO, 1)
      return
    for switch in self.switches:
      self.log("Entities :  {}".format(switch), level="INFO")
      self.listen_state(self.updatecontext, switch)
    
  def updatecontext(self, entity, attribute, old, new, kwargs):
      context=self.get_state(entity, attribute="context")
      self.id=context["id"]
      self.parent_id=context["parent_id"]
      self.user_id=context["user_id"]
      self.log("Current Entity : {} {}".format(entity, context), level="INFO")
      self.log("ID : {}".format(self.id), level="INFO")
      self.log("parent_id : {}".format(self.parent_id), level="INFO")
      self.log("user_id  : {}".format(self.user_id), level="INFO")

      if self.id is not None and self.parent_id is None and self.user_id is None:
        self.interaction="physical"
        self.user="unknown"
      if self.id is not None and self.parent_id is not None and self.user_id is None:
        self.interaction="automation"
        self.user="unknown"
      if self.id is not None and self.parent_id is None and self.user_id is not None:
        self.interaction="ui"
        self.user=self.user_id
      self.log("interaction  : {}".format(self.interaction), level="INFO")
      self.set_state(entity, attributes = { "interaction" : self.interaction })
      self.set_state(entity, attributes = { "user" : self.user })

If these are existing switches that are being created in HA with an integration, you wont want to modify their attributes. Instead, you can create new sensors using set_state() within AD and keep track of the information that you need. Any attribute changes made within an integration will be overwritten or even worse, could cause the integration to not work properly.

Hi
This is adding custom attributes . Not modifying existing ones :slight_smile:
However They are added on existing switches .
Do I understand that could cause trouble?

Correct, you should not use set_state() to modify existing entities that are controlled by an integration. The integration that creates the entities is technically the “single source of truth” and you break that if you start modifying the entities from a different location. It is not a technical limitation so AD will let you do it but you’re likely to have adverse and unpredictable behavior. Integrations should be controlled by calling services.

:frowning: so far it seemed not to interfere.
Once again I have a switch entity created for example by zwave integration and I add 2 new custom attributes to the entity :frowning:
I have seen this done also via python scripts in the past . So it is potentially breaking

Yeah, it might not break anything but if you aren’t aware of all the implementation details of the integration then you can be introducing problems without knowing it. Also, just because other folks do it doesn’t necessarily mean it’s correct either. Anytime I have extra data that is associated with an entity (or even if it’s not), I create a sensor within AD’s “user defined namespace” and store it there. Attributes are stored as a dictionary so you could store the entity name as a key and the remaining data as values such as, set_state("sensor.entity_metadata", attributes={"switch.from_integration": {"parent_id": 1}}, namespace="udn_namespace"). If you just need the data within AD then nothing left to do as you can access multiple namespaces. Unfortunately if you need the data available in HA and you need the data to be persistent even across reboots, you’ll want to store in AD’s UDN and copy it to the HA namespace. Hopefully that make sense.

1 Like

Aha good technique. Just learning appdaemon and to have persistence I was using python shelve lib.
This is way cleaner

There are lots of little tricks in AD, feel free to join our discord group as well.

This thread really helped me out, I have had trouble finding clear documents on how to create a sensor with attributes from Appdaemon. The namespace information didnt really help me either!

To start off, I am trying to track the reason why lights are activated. i.e. Automation based on luminosity or motion, or manual intervention via a switch. I need this information to be non-volatile and available in HA so another HA based (not AD) automation can access it.

I currently have the sensor being created, but the attributes are not there.

        # entity is the name of my entity, coming from a listen_state callback
        meta_sensor = entity.replace(".","_")
        meta_sensor = f"sensor.{meta_sensor}_metadata"
        self.set_state(meta_sensor, state = "Appdaemon")
        self.set_state(meta_sensor, attributes = { "on_manually" : self.on_manually })
        self.set_state(meta_sensor, attributes = { "off_manually" : self.off_manually })
        self.set_state(meta_sensor, attributes = { "on_for_motion" : self.on_for_motion })
        self.set_state(meta_sensor, attributes = { "on_for_luminosity" : self.on_for_luminosity })

Here is the Developer Tools States page

I’ve tried adding namespace=“udn_namespace” to the set_state calls, but then they dont turn up in HA at all. And I do not understand @proggie’s comment

Unfortunately if you need the data available in HA and you need the data to be persistent even across reboots, you’ll want to store in AD’s UDN and copy it to the HA namespace. Hopefully that make sense.

If I create this in a udn_namesapce, how do I copy it to HA namespace?

OK, feel really silly. :slight_smile:
It was only the front end not updating, literally immediately after my post I hit the refresh button and the attributes appeared (see yellow highlight)

2 Likes

Here’s a new version.
Creates binary_sensors.
The sensors will contain both interactions as the previous version
Furthermore there will be a “clicks” properties that contain the number of clicks in a timespan defined in the configuration.

type or paste #
# Swint
# 
# V1.0
# Create a sensor for each switch passed as input containing
# Value: 
# True and Then False upon Switch Interaction (Physical/Automation/UI). You can use as a kind of event to catch in your automations
# 
# Attributes: 
# Interaction Type
#           Automation : The switch was toggled  via an automation
#           Physical :  The switch was toggled via physical interaction
#           UI :  The switch was toggled via User Interface
# User
# User that interacted in case of UI. Unknown for physical or automation interaction
# Clicks 
# The number of clicks registered on the switch, starting from the first and within a maxtime seconds window
#
# Interaction decoding based on
# ID combos: https://community.home-assistant.io/t/work-with-triggered-by-in-automations/400352/8
#interaction	id	        parent_id	    user_id
#Physical	    Not Null	Null	        Null
#Automation	    Not Null	Not Null	    Null
#UI	            Not Null	Null	        Not Null
#
# Sample config section for apps.yaml:
#switchint:
#  module: switchint
#  class: switchint
#  switches:
#    - light.light1
#    - light.light2
#    - light.light3
#    - light.light4
#  usersid_users:
#      c600656764c74eeeb08a964843fa8771: user1
#      8bfa609e73d644a9bf2e119bb7040491: user2
#  maxtime: 5
#  logging: logging level 0 none, higher more verbose


# TODO: better input check on entities, translate userid to friendly name of user

import hassapi as hass
from datetime import datetime, timedelta


class swint(hass.Hass):
  def initialize(self):
    try:
        self.loglevel = self.args.get("loglevel",4)
    except:
      self.log("Error opening parameters!")
      return
    self.mylog("*******************",0) 
    self.mylog("Initializing swint.",0) 
    self.mylog("*******************",0) 
    self.mylog("Log Level:".format(self.loglevel),0)
    try:
      self.switches = list(self.args["switches"])
      self.usersid_users_dict = dict(self.args.get('usersid_users', None))
      self.maxtime = self.args.get("maxtime")
    except:
      self.log("Error opening parameters")
      return

    self.switchclicks = { listel : 0 for listel in self.switches }
    self.mylog("Switches  {}".format(self.switches),1)
    self.mylog("Maxtime  {}".format(self.maxtime),1)
    self.mylog("Switchclicks  {}".format(self.switchclicks),1)
    for switch in self.switches:
      self.mylog("Listening interactions for :  {}".format(switch),1)
      self.listen_state(self.updatecontext, switch)
      self.mylog("Created Entity {}.".format("binary_sensor.il_"+switch.replace(".","_")),1)
      self.set_state("binary_sensor.il_"+switch.replace(".","_"), state ="off",
          attributes={
          "device_class": None,
          "unique_id": "binary_sensor.il_"+switch.replace(".","_"), 
          "name": switch.replace(".","_"), 
          "last_changed": self.datetime().replace(microsecond=0).isoformat(),
          "clicks": 0 }
      )
      self.switchclicks[switch] = 0
      self.listen_state(self.listenclicks, switch)
      self.listen_state(self.updatecontext, switch)
    self.mylog("Now ListeningG to events.",1)
    
  def updatecontext(self, entity, attribute, old, new, kwargs):
      context=self.get_state(entity, attribute="context")
      self.id=context["id"]
      self.parent_id=context["parent_id"]
      self.user_id=context["user_id"]
      self.mylog("Current Entity : {} {}".format(entity, context),1)
      self.mylog("ID : {}".format(self.id),1)
      self.mylog("parent_id : {}".format(self.parent_id),1)
      self.mylog("user_id  : {}".format(self.user_id),1)

      if self.id is not None and self.parent_id is None and self.user_id is None:
        self.interaction="physical"
        self.user="unknown"
      if self.id is not None and self.parent_id is not None and self.user_id is None:
        self.interaction="automation"
        self.user="unknown"
      if self.id is not None and self.parent_id is None and self.user_id is not None:
        self.interaction="ui"
        self.user=self.user_id
      self.mylog("interaction  : {}".format(self.interaction),1)
      self.mylog("Sensor  : {}".format("binary_sensor.il_"+entity),1)
      self.set_state("binary_sensor.il_"+entity.replace(".","_"), state ="off", attributes = { "interaction" : self.interaction })
      if self.user_id is not None:
        self.set_state("binary_sensor.il_"+entity.replace(".","_"), state ="off", attributes = { "user" : self.usersid_users_dict[self.user_id] })
      else:
        self.set_state("binary_sensor.il_"+entity.replace(".","_"), state ="off", attributes = { "user" : "unknown" })
      self.mylog("In Update Context Entity {} Clicks {}".format(entity,self.switchclicks[entity]),1)



  def listenclicks(self, entity, attribute, old, new, kwargs):            
      self.mylog("Listening for Entity {}".format(entity),1)
      self.mylog("Current switchclicks {}".format(self.switchclicks[entity]),1)
      if self.switchclicks[entity] == 0:
          self.switchclicks[entity] = 1
          self.mylog("Set callback for Entity {} in  {}".format(entity, self.maxtime),1)
          self.run_in(self.clickcount, self.maxtime,  myentity = entity)            
      else:
          self.switchclicks[entity] = self.switchclicks[entity] + 1
      self.mylog("In Listenclcks Entity {} Clicks {}".format(entity,self.switchclicks[entity]),1)


  def clickcount(self, myentity):
      self.mylog("switchclicks {} ".format(self.switchclicks),1)
      self.mylog("MyEntity {} ".format(myentity),1)
      self.mylog("Final Clicks for {} : {}".format("binary_sensor.il_"+myentity["myentity"].replace(".","_"), self.switchclicks[myentity["myentity"]]),1)
      self.set_state("binary_sensor.il_"+myentity["myentity"].replace(".","_"), 
          state="on",
          attributes={
          "device_class": None,
          "unique_id": myentity["myentity"]+"_Interaction", 
          "name": myentity["myentity"]+" Interaction", 
          "last_changed": self.datetime().replace(microsecond=0).isoformat(),
          "clicks": self.switchclicks[myentity["myentity"]] }
      )
#      self.fire_event("clicks", entity=myentity["myentity"], clicks=self.switchclicks[myentity["myentity"]])
      self.mylog("RiRiFinal Clicks {}".format(self.switchclicks[myentity["myentity"]]),1)
      self.switchclicks[myentity["myentity"]]=0
      self.set_state("binary_sensor.il_"+myentity["myentity"].replace(".","_"), state="off")
      
  def mylog(self,logtxt,thislevel):
      if self.loglevel >= thislevel:
          self.log(logtxt,log="dev_log")

config

##========================================================================================
##                                                                                      ##
##                                        Swint                                         ##
##                                                                                      ##
##========================================================================================
swint:
  module: swint
  class: swint
  switches:
    - light.luce_cucina
    - light.led_pensili
  usersid_users:
      c600656764c74eeeb08a964843fa8771: Alessandro
      8bfa609e73d644a9bf2e119bb7040491: admin
  loglevel: 3
  maxtime: 2

should be self explaining