appdaemon icon indicating copy to clipboard operation
appdaemon copied to clipboard

callback filtering with custom attributes

Open PremiumUsername opened this issue 6 years ago • 23 comments

I know that you can limit how often some callbacks are called by filtering with attributes. The problem is, while HA allows you to create custom attributes with:

customize.yaml:

input_select.roller_blinds_balcony:
  event_type: roller_blinds

...it does not propagate those attributes when it then fires events. So I can't use them to filter callbacks. in order to use them, all I can do is to stop a callback early with:

ad_app.py

class RollerBlinds(hass.Hass):

    def initialize(self):
        self.listen_event(self.event_received, 'call_service', domain = 'input_select')

    def event_received(self, event, data, *args):

        entity_id = data['service_data']['entity_id']

        #checks if custom attribute "event_type" is set to the right thing, otherwise abort            
        if self.get_state(entity_id, attribute = "event_type") != 'roller_blinds':
            self.log('wrong input select: aborting')
            return

If AD was made to be aware of custom attributes in such a way that it could be used for callback filtering (i.e. not just extracting the attributes from the event firing) I think that would be a pretty nifty enhancement of the callback functionality. Then you could just tag the entities in HA that you would a like a certain AD app to listen to with a custom attribute. What do you think?

PremiumUsername avatar Aug 13 '19 12:08 PremiumUsername

why use listen_event at all? just use:

class RollerBlinds(hass.Hass):

    def initialize(self):
        self.listen_state(self.event_recieved, "input_select.the_one_you_use", attribute="event_type", new="roller_blinds")

    def event_received(self, entity, attribute, old, new, kwargs):
        self.log("event_type from input_select changed to rollerblinds")

by the way a callback for an event should be:

   def event_received(self, event, data, kwargs):

and not *args

ReneTode avatar Aug 13 '19 15:08 ReneTode

deleted my previous response since I misunderstood completely

I realize now that listen_event was a poor choice on my part. When I monitored the logs, that's what I saw so I went with that.

I've tested your code and it does work in filtering callbacks. So this is indeed a solution. It does, however, seem like a convoluted solution to monitor an attribute that never changes for changes. It does work, but to me it seems like this solution is taking advantage of a bug . Why is the callback firing when the attribute never changes? The docs say "...will subscribe to changes for just that attribute within that specific entity". It doesn't do that. What if you do want to monitor just for changes in one attribute, then another attribute changes and the callback fires again since it sees the matching "new" status for the monitored attribute.

PremiumUsername avatar Aug 13 '19 18:08 PremiumUsername

when the attribute never changes, then how would you get a response, and why would you want to listen to it?

hmm, i think now i get what you tried to do. you listen to all input_select changes and want only the one with the attribute rollerblinds

and i forgot about the bug in AD 3.0.5 that has to do with attributes.

but in your case, just listening to your input_select (instead of listening to everything and then start selecting) would be enough.

self.listen_state(self.event_recieved, "input_select.the_one_you_use")

add a listen_state for all entities that you need.

and the reason why the listen_event istnt working is because attribute is a subdict inside the data, so its hard to select on any attribute (custom or not)

in general: if you want to listen to state changes, use listen_state and not listen_event.

ReneTode avatar Aug 13 '19 23:08 ReneTode

you listen to all input_select changes and want only the one with the attribute rollerblinds

Yes, this is corrects.

add a listen_state for all entities that you need

That would work, but it wasn't what I wanted to do. It seems a bit cumbersome if you have many entities you need to include in the filter.

the reason why the listen_event istnt working

My listen_event is working just fine, but I agree that I should have used listen_state instead, so I didn't have to fish the entity out of the subdic. My feature suggestion is a much better fit for listen_state, so let's forget about listen_event for now.

I just thought that the way I use a custom attribute for filtering (which is working), would be even better if it was a functionality of AD itself in order to avoid the unwanted callbacks altogether.

something like:

self.listen_state(self.event_recieved, entity = 'input_select', attribute_value_required = {"event_type" : "roller_blinds"})

Or maybe AD could just assume one custom attribute name is meant for this purpose so that if there is one custom attribute called e.g. AD_filter then you would only need:

self.listen_state(self.event_recieved, entity = 'input_select', filter_attribute_required = "roller_blinds")

PremiumUsername avatar Aug 14 '19 06:08 PremiumUsername

to filter callbacks we have constraints. in this case you want a custom constraint, that checks if an certain attribute has a certain value. https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#callback-constraints

so the option exists, only in another way you were looking for.

ReneTode avatar Aug 14 '19 08:08 ReneTode

I'm sorry. I can't see that option. I see:

input_boolean input_select presence time

How do you use a certain attribute for a constraint?

PremiumUsername avatar Aug 14 '19 11:08 PremiumUsername

custom constraint. https://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#custom-constraints

and we are talking about adding more default options for constraints.

ReneTode avatar Aug 14 '19 12:08 ReneTode

Maybe I'm missing something but all those constraint including the custom ones are general in nature. So I can't see how you could use a custom constraint to check the attribute of only the entity that activated the callback filtering function.

PremiumUsername avatar Aug 14 '19 12:08 PremiumUsername

with a custom constraint you create a function that gives back true or false based on the given input. by adding a listen_state for all input_selects (which you can get with get_state before the callback) in a for loop you know which entity you are listening to, so you can give that same entity in your constraint input. so the initialise would become something like:

input_selects = self.get_state("input_select")
self.register_constraint("custom_constraint")
for input_select in input_selects:
    self.listen_state(self.CB, input_select, custom_constraint = {"entity": input_select, "event_type": "rollerblinds"})

and you would have a function in the same class like:

def custom_constraint(self, conditions):
  if self.get_state(conditions["entity"], attribute = "event_type") == conditions["event_type"]:
    return False
  else:
    return True

ReneTode avatar Aug 14 '19 13:08 ReneTode

by adding a listen_state for all input_selects

Yes, but then why would I need to filter with constrain? I could just use get.state to exclude the ones I don't want. Much like I do now, only after the callback.

PremiumUsername avatar Aug 14 '19 13:08 PremiumUsername

because that way the callback wont even be started. all a matter of choices. but in AD 4 you will see the difference between a callback that is returned inside the callback and a callback that is never fired.

we are working on other default constraints also, but i dont think it will be one like you would need.

ReneTode avatar Aug 14 '19 13:08 ReneTode

My point was that if you were just to make a loop to discover ahead of time what entities should have callbacks based on the custom attribute, you would not need to bother with the constrain. No false callbacks would be made in that case.

In any case there are many ways to solve this. I'm looking forward to see what you come up with in AD 4.

PremiumUsername avatar Aug 14 '19 13:08 PremiumUsername

One possible additional advantage with using a custom attribute is that it would be very easy to alter from one AD app what entities would be included in future callbacks for that AD app or any other. Say that I want to exclude some entities from future callbacks for some reason, then you could only use set.state for those and change that attribute.

PremiumUsername avatar Aug 14 '19 13:08 PremiumUsername

you are right. selecting before gets rid of the need for the constraint. allthough the constraint could get have other options as well. indeed for everything are several ways to go.

and i know the advantage from custom attributes. i use them mostly on sensors though, because when you do set them in HA, you need to restart HA every time you add or change something, and adding attributes from within AD to anything but sensors is quite unreliable. HA tends to delete attributes that are not inside its own config.

ReneTode avatar Aug 14 '19 15:08 ReneTode

HA tends to delete attributes that are not inside its own config

Yes, I've noticed that. I haven't actually tried changing an custom attribute created in HA from AD. Maybe HA will just reset it the first opportunity it gets. Hopefully HA will be more accommodating to AD created entities and attributes in the future.

PremiumUsername avatar Aug 14 '19 16:08 PremiumUsername

Hopefully HA will be more accommodating to AD created entities and attributes in the future

dont count on it. i deal with that problem for over 2 years now. the better way is to just create entities with AD. those dont get overwritten.

ReneTode avatar Aug 14 '19 19:08 ReneTode

I think I'm looking for similar functionality, both for custom attributes defined in HA's customize.yaml as well as with the default attributes.

Simplest case being I'd like to listen/act on state changes for all binary_sensors which have the attribute device_class=motion only. I expected this to be possible using something like:

self.listen_state(self.motion, "binary_sensor", new="on", required_attributes={"device_class": "motion"})

Since that's not the case I'm now just filtering the entities once the state change event triggers.

simonvanderveldt avatar Oct 05 '19 16:10 simonvanderveldt

a better way is:

for entity in self.get_state("binary_sensor"):
  if "motion" in entity["attributes"]:
    self.listen_state(self.motion, entity, new= "on")

off course its possible to create a new arg for listen_state, but is that really needed for something that can be done with 2 lines of code?

ReneTode avatar Oct 06 '19 11:10 ReneTode

@ReneTode 's method works well as long as your HASS instance doesn't create new binary_sensors after it's started. If they are created a bit later (slower components, or components that adapt to changes in the upstream system while HASS is live) then this block of code will miss them.

However, putting your filtering at the top of the callback is not detrimental to system performance. There are a few things to note:

Home Assistant sends every state change to AppDaemon whether you're listening to it or not. This is how Home Assistant works and, as of right now, there's no way to change this. So, even without any listen_state's at all active, AppDaemon still has work to do when a state change happens.

  1. It sees the state change,
  2. updates the internal state of that entity
  3. loops it's list of listeners (which would be empty if you had no listen_states),
  4. looks at the entity and attribute that was being listened to to ensure it changed,
  5. analyzes any constraints (including "new" and "old" parameters) to see if there is a match,
  6. and finally calls the callback.

Even with no listen_state in any app, steps 1 and 2 are happening no matter what. Step 3 and 4 will happen whether you have 1 listen_state or 1,000. If you have a listen_state('binary_sensor'), then Step 5 will happen for every binary_sensor. If you have listen_state('binary_sensor.one_thing'), then step 5 only happens when there is a change for that one entity. Step 6 only happens when everything matches.

If entity_attribute filtering were added to the AppDaemon core, Step 1-5 would still happen and that extra filtering would happen as part of Step 5. So, with it in place, you'd only be saving Step 6. And since step 6 is your callback, it's as efficient as you make it. By checking that the attribute is present and set to a specific value at the top of the callback, it's not really any more expensive than if AppDaemon were to do that itself. So it's not really a performance concern to use listen_state('binary_sensor') and perform any extra checks you need at the top of the callback.

dlashua avatar Oct 06 '19 12:10 dlashua

That being said, allowing for context in a constraint could make this easier. The following code doesn't work, currently, but perhaps it should:

class MyApp(hass.Hass):
  def constrain_attribute(self, value, context={}):
    attribute_value = self.get_state(context['entity_id'], attribute=value['attribute'])
    return value['value'] == attribute_value

  def initialize(self):
    self.register_constraint('constrain_attribute')
    self.listen_state(self.cb, 'binary_sensor', constrain_attribute={"attribute":"device_class", "value":"motion"})

The difficulty would be implementing this without breaking all existing constraints and requiring users to add the "context" parameter to their defined constraints.

dlashua avatar Oct 06 '19 12:10 dlashua

By checking that the attribute is present and set to a specific value at the top of the callback, it's not really any more expensive than if AppDaemon were to do that itself. So it's not really a performance concern to use listen_state('binary_sensor') and perform any extra checks you need at the top of the callback.

not in AD 3, but you forget the AI steps when it comes to AD 4.

its just better practice to ensure that HA does have all the entities you want to work with, before your apps are initialising. (add a delay if needed, add a check for the amount of needed entities, add a check for slow initialising entities in HA, add a check for 2 minutes after initialising the app if new entities are added, or ... there are lots of options to make sure that you got every entity that you need) and its also better practice to reduce the amount of callbacks to a minimum. maybe not on a system that doesnt do much, but if you got a system like mine it makes a BIG difference.

or do you think it doesnt make a difference when you get 150.000.000 webchanges a year against 25.000.000 a year?

and off course with those amount of callbacks, every line of code you put in there to see if it needs to run or not, will make a difference too.

ReneTode avatar Oct 07 '19 09:10 ReneTode

Since my AppDaemon CREATES some of the entities I'd like to check, this gets painfully difficult. On top of that, when I add devices with some components, they add to Home Assistant right then. If I code it this way, I'll have to restart Home Assistant when these new devices are seen.

There are ways to detect new devices (listening to _AD_ENTITY_ADDED, for instance) but, it's a nice hunk of code. It's much easier to just check in the callback if a constraint can't handle it.

And yes, between Step 5 and 6 all of the admin callback stats are updated, which is a good reason to enhance constraints in some way to be able to handle this (hence my comment above with a possible implementation of this). But, with that being said, the callback stat updates are quite efficient. I have seen my callbacks/sec count reach 12 at times, and still, with these kinds of checks in many of my callbacks, they run quite well with no perceived delays (many of them handling motion sensor based lights, where even a little delay is very easily noticed).

dlashua avatar Oct 07 '19 17:10 dlashua

Since my AppDaemon CREATES some of the entities I'd like to check, this gets painfully difficult

no it isnt. a litteraly create hundreds of entities with appdaemon. and ALL are created BEFORE any other initialisation takes place. its just not smart to create entities all over the place inside all kind of apps on any kind of time. how would HA look if it only starts to create entities when an automation calls for them? its possible to create entities with AD into HA, but its just good practise to make sure they are there before you need them. same for entities added in any other way. how would your conditions or a listen_state ever work correctly if the entities they depend on dont exist? how will you ever have a reliable constraint, when the entities that are used to constrain dont exist?

you cant enhance constraints to constrain on not existing entities. and you really dont want constraints to behave differently based on how much from your code is already started, or running.

i now the callback stats update efficiently. i got moments that its at 30 or 40. i really dont want that to go to 100 or more because i need to check inside unneeded callbacks. do you run the AI 24/7? i do. and the recieving device has a hard time handling all that incoming code correctly, if it gets to high.

please dont start telling people: heh, AD is powerfull enough, dont worry about callback amount. even though AD is powerfull enough, its still good practice to reduce the amount of callbacks any way you can.

so only listening to the sensors that are needed, is better then listen to all and sort it out in the callback. and there ARE ways to make sure all entities exist before you start listening. that you dont want to bother, doesnt make it good practice.

ReneTode avatar Oct 07 '19 23:10 ReneTode