Advice on app design please

Hi @Odianosen25 - no, I’m afraid life got in the way and I haven’t had a chance to make any real progress on this yet.

In fact, I am not actually convinced that @andrewjfreyer’s script publishing declining probabilities is the right approach. My thinking was to ignore anything other than 100% confidence (ie a BT presence is confirmed) and move the logic of whether a non-100% confidence implies absence to the python code. One of the reasons for this is that I would like to include other-sensors (in particular a network connection) in my presence determination. (So, for example, if my phone connects to wifi then I am present, regardless of what the script confidence is. But if all of the confidence publications are non-100 and an external door has recently opened then I am absent, regardless of whether my router thinks I am wifi-connected.)

I’d be very interested to hear if you manage to make any progress - and should have time for some testing if you need it.

Wow that’s some complexity for presence detection :grimacing: @PianSom , but you make a valid point. As I just setup the script yesterday and going via what it published, though I was home it sometimes registered me as 83%. So the ability to combine all these in code, seems valid to be honest.

@PianSom so I don’t hijack the other thread anymore :smiley: I have had really really good results with @andrewjfreyer script. So much so, I don’t even bother with the unifi tracker anymore for anything. Running just the script on the zero’s with nothing else on them, and don’t seem to have any major wifi interference. Have it flipping a input boolean as to if I am home or not after a 5 minute grace period, and this is the last week matched up to the unifi. It picks up when I go to the shop round the corner which the unifi can’t.

device%20home

2 Likes

Hi @Odianosen25

So, I have made a start on coding. I abandoned my original idea of keeping track of @andrewjfreyer’s probability of presence, and decided that it was easier to just look at 100% certainty but to have a time delay before changing the presence boolean.

What happens is -

  • the monitor script runs on three Pi Zeros, and posts to MQTT in a vanilla fashion
  • I use unifi2mqtt to post to MQTT whether a phone device is present on the network
  • in HA, MQTT subscriptions listen for changes in the above postings and flip a sensor Seen/Not seen as appropriate (NB Seen for monitor means 100% certainty)
  • the code below changes a presence boolean if, for a given person, (a) any sensor is showing Seen then the presence boolean is set to TRUE (and any timer previously set up for the next condition is cancelled), or (b) if all sensors are showing Not seen then a timer is set for n minutes, after which the boolean is changed to FALSE

With apologies in advance for my very basic Python coding (I can just feel the stinging @ReneTode rebuke now! :slight_smile: ) here is where I am:

class person_presence(hass.Hass):

  def initialize(self):
    self.log("Hello from person_presence", level='DEBUG')
    self.listen_state(self.change_boolean, self.args["network_device"])
    self.listen_state(self.change_boolean, self.args["bluetooth_device_kitchen"])
    self.listen_state(self.change_boolean, self.args["bluetooth_device_utility"])
    self.listen_state(self.change_boolean, self.args["bluetooth_device_upstairs"])


  def change_boolean(self, entity, attribute, old, new, kwargs):
    self.log("Potential change of state for {}".format(self.args["person_boolean"]))
    if self.get_state(self.args["network_device"]) == self.args["home_state"] or \
       self.get_state(self.args["bluetooth_device_kitchen"]) == self.args["home_state"] or \
       self.get_state(self.args["bluetooth_device_utility"]) == self.args["home_state"] or \
       self.get_state(self.args["bluetooth_device_upstairs"]) == self.args["home_state"]:
      self.person_present(self)
    elif self.get_state(self.args["network_device"]) != self.args["home_state"] and \
         self.get_state(self.args["bluetooth_device_kitchen"]) != self.args["home_state"] and \
         self.get_state(self.args["bluetooth_device_utility"]) != self.args["home_state"] and \
         self.get_state(self.args["bluetooth_device_upstairs"]) != self.args["home_state"]:
      self.person_poss_absent(self)

  def person_present(self, kwargs):
    self.log("Present: Immediately updating {} from {} to on, and cancelling Poss Absent".format(self.args["person_boolean"], self.get_state(self.args["person_boolean"]) ), level='DEBUG')
    self.turn_on(self.args["person_boolean"])
    try:
      self.log('Going to cancel a Poss Absent action for {}'.format(self.args["person_boolean"]), level='DEBUG')
      self.cancel_timer(self.handle)
    except AttributeError:
      self.log('Tried to cancel a Poss Absent action for {}, but none existed'.format(self.args["person_boolean"]), level='DEBUG')

  def person_poss_absent(self, kwargs):
    self.log("Possibly Absent: Updating {} to from {} to off in {} mins".format(self.args["person_boolean"], self.get_state(self.args["person_boolean"]), self.args["away_delay"] ), level='DEBUG')
    timer=60*self.args["away_delay"]
    self.handle=self.run_in(self.person_absent, timer)

  def person_absent (self, kwargs):
    self.log("Absent: Turning off {} now".format(self.args["person_boolean"]), level='DEBUG')
    self.turn_off(self.args["person_boolean"])

with sample yaml:

pian_presence:
  module: presence
  class: person_presence
  network_device: sensor.unificat_pianiphone
  bluetooth_device_kitchen: sensor.kitchen_pianiphone
  bluetooth_device_utility: sensor.utility_pianiphone
  bluetooth_device_upstairs: sensor.upstairs_pianiphone
  home_state: 'Seen'
  person_boolean: input_boolean.pianathome
  away_delay: 5

Next steps

  • I need to spend some more time tuning monitor and working out whether to use non-default settings to trigger bluetooth scans more efficiently (eg when another sensor suggests a change in presence is imminent; I have got some cheap IBeacons to put in the car, which I am hoping will have enough range to spot me before I get home)
  • the Unifi router presence is not very reliable. I think there are a couple of branches of the base code worth exploring
  • I would love to do away with the HA MQTT interface and just use AD for MQTT. One day …

:laughing: I am so telling him you said that :stuck_out_tongue_winking_eye:

This is possible now

Never knew about this, why use it if just sticking to the presence script alone?

Good start, will look into it and make some mods of my own and post later. Haven’t had the time to work on this side of things.

But it seems your code works off the back of the user setting up different things in HA and AppD. And if there are multiple ppl in the home, it just escalates more and to me that is a drag :tired_face:

Lets see how it goes, and I will endeavour to send update on this.

Thanks again for the start

Oh, really? Tell me more. I can’t see anything in the AD docs about having a MQTT interface. I’d much prefer to do all my listening and processing in AD and then just use MQTT as an output for HA to pick up.

I agree that the the whole thing is a bit held together by string (with multiple sensors all over the place) and I would much rather have something simpler and easier to configure. Having AD directly subscribing/publishing with MQTT would be fantastic.

[EDIT - unless you meant using this type of thing??]

Because for me wifi sometimes has longer range than Bluetooth, so especially if I am approaching on foot I will connect by wifi before monitor spots me. (The downside is that the router often takes longer to realise I am gone, so when I refine my [lack of] presence algorithm from a simple AND I will take that into account.)

Nope this

Since it takes longer, might just use it for only when it registers u home and the others are still giving a presence of less than 100% and ignore the other.

Regards

Oooh! So I can already subscribe to MQTT in AD?

And all I have to do to do something when a presence/owner/pi0ups/XX:XX:XX:XX:XX:XX is
1 - declare a MQTT section in appdeamon.yaml which includes the topics I am interested (eg presence/#) in, and
2 - in an initilalize have a self.listen_event(self.some_function,'presence/owner/pi0ups/XX:XX:XX:XX:XX:XX')

And then I can publish at will using self.mqtt_send(topic, payload, qos=0, retain=false, kwargs)

Is that right? That will simplify things a lot.

Correct

Not exactly, but these:

First you need to move the plugin files into your plugin directory. normally one should be able to just put custom plugins into a custom_dir folder in the appdaemon conf directory, but I haven’t been able to get that to work, so still trying to figure that out.

So what I do is to load mine into my directory which is /home/pi/srv/appdaemon/lib/python3.5/site-packages/appdaemon/plugins if using hassbian. Still waiting on @aimc or @ReneTode to show me otherwise. Till then well this is all I know.

Your listen function is more like this:
self.listen_event(self.some_function,<event name as specified in appdaemon.yaml('MQTT' default)>, topic = 'presence/owner/pi0ups/XX:XX:XX:XX:XX:XX')

Below is a sample I use for my TTS at home

import appdaemon.plugins.mqtt.mqttapi as mqtt
import json

class TTSApp(mqtt.Mqtt):

  def initialize(self):
    self.set_namespace('mqtt')
    self.speed = self.args['speed']
    self.pitch = self.args['pitch']
    self.allowed_sites = self.app_config["snips_data"]["sites"]
    for site in self.allowed_sites:
        self.listen_event(self.tts_send_message, 'MQTT', topic = 'ha/tts/{}/say'.format(site.lower().replace(' ', '_')))
    
  def tts_send_message(self, event_name, data, kwargs):
    topic = data['topic']
    text = "<pitch level=\'{}\'><speed level=\'{}\'>{}</speed></pitch>".format(self.pitch, self.speed, data['payload'])
    self.log(data['payload'], level='INFO')
    siteId = topic.split('/')[2]
    payload = json.dumps({"siteId":siteId, "text":text})
    self.mqtt_send('hermes/tts/say', payload, namespace = 'snipsmqtt')
1 Like

please dont apologise for simple coding. thats the best there is :wink:
and dont forget, i love helping people improve but i am not even close to being an expert (but in the land of the blinds oneeye is king)

i would probably write the app the same when i start writing it.
then i would start optimising for readability.

i would place the devices in a list in the yaml and then use this:

  def initialize(self):
    for device in self.args["devices"]:
        self.listen_state(self.change_boolean, device)
    self.log("Hello from person_presence", level='DEBUG')

in change_boolean then you could use

Person_present = False
for device in self.arg["devices"]:
    if self.get_state(device) == self.args["home_state"]: 
        Person_present = True
if Person_present:
    self.person_present(self)
else:
    self.person_poss_absent(self)

the big advantage is that you now can easy add or delete devices in the yaml without changing the code.
which also would make the code more sharable.

about the MQTT part: i havent played around with it enough to be able to get into it.
i have a working mqtt app, but not based on the pluging, but just with:

import paho.mqtt.client as mqtt
import paho.mqtt.publish as publish
import paho.mqtt.subscribe as subscribe

i also have the plugin (same way as @Odianosen25 ) and an app based on that but i created it a while ago and dont know for sure if it is working :wink:

1 Like

@ReneTode,

Thanks for the input. The plugin works really great as my system is like 60% MQTT especially considering I use Snips and Zigbee2mqtt.

It makes it easier for me to manage things, and I connect to two different brokers at the same time. That’s why I have different “namespaces”, snipsmqtt and mqtt. And I am in the process of adding a third cloud based on.

Regards

2 Likes

Thanks @ReneTode - much more elegant and efficient. I knew your insight would be valuable. (In spite of what @Odianosen25 thinks :grin: )

I hadn’t realised that the MQTT stuff was in a custom component. I try to avoid these, as it’s yet another thing to make sure continues working over time. My current method - cumbersome though it is - at least is based on core functionality. Hopefully AD will natively include MQTT functionality at some stage, because - like @Odianosen25 - virtually all of my IoT devices are MQTT-based .

2 Likes

My plan is not to integrate the MQTT plugin, not because I don’t see the value - I totally do and Ilove the plugin. Problem is as I integrate more and more 3rd party stuff (I don’t even use MQTT) it becomes harder to maintain, and there is only one of me.

So, for Skins, plugins etc. all third part addons to MQTT my preference is to have them owned by someone that uses them who will commit to keeping it up to date over time - I was planning on asking @Odianosen25 if he would be interested in doing this :slight_smile:

What do you think @Odianosen25?

Hello @aimc,

I am totally cool with it, as it is my life blood and I am working on a product I plan giving users based on it, I will be glad to take it up to be honest.

regards

1 Like

I get your point @PianSom, but in the event you still interested, I have just put together this app.

Now its totally untested as I will look into it when I get home, but what it does it thus:

  1. it will check for users based on location
  2. create confidence sensors in HA per user, per device, per location.
  3. This way, no need to modify your sensors.yaml as AppD does it for you
  4. Based on the confidence reading it gets, per device per location, it will determine if the user in home or not
  5. A sensor created to indicate the user is home or not is available.

My idea is that the only thing one should modify is the owner_devices or guest_devices files and you should be good to go.

To do:
Test for bugs (of course :roll_eyes:)
This only works on a per user per device, as one might have multiple devices. So I will work on modifying the code later, so devices can be grouped together for a single user.

See blow for updated code

1 Like

Excellent and thanks :slight_smile: I need to finalize and debug the custom_plugin stuff, I havent looked at it for a while. When we have that sorted out, I’ll Message you about how we can set this up.

Thanks again!

1 Like

Well @Odianosen25 - I certainly feel chastened now! :slight_smile:

I will study your code and put a cold towel on my head.

@PianSom I am sorry bro :wink:, I didn’t mean to

1 Like

@PianSom, here is the debugged version, as I had some time to test it out. If you don’t mind, I will delete the previous one in the other comment, so others don’t use it.

apps.yaml

home_presence_app:
  module: home_presence_app
  class: HomePresenceApp
  plugin: HASS
  presence_topic: presence
  database: /home/pi/.appdaemon/conf/apps/settings/database
  minimum_confidence: 100
  not_home_timeout: 60

home_presence_app.py

import appdaemon.plugins.mqtt.mqttapi as mqtt
import json
import shelve

class HomePresenceApp(mqtt.Mqtt):

    def initialize(self):
        self.set_namespace('mqtt')
        self.hass_namespace = self.args.get('hass_namespace', 'default')
        self.presence_topic = self.args.get('presence_topic', 'presence')
        self.db_file = self.args['database']
        self.listen_event(self.presence_message, 'MQTT')
        self.not_home_timers = dict()
        self.timeout = self.args.get('not_home_timeout', 120) #time interval before declaring not home
        self.minimum_conf = self.args.get('minimum_confidence', 90)

        with shelve.open(self.db_file) as db: #check sensors
            remove_sensors = []

            try:
                sensors = db[self.presence_topic]
            except:
                sensors = {}

            if isinstance(sensors, str):
                sensors = json.loads(db[self.presence_topic])

            for conf_ha_sensor, user_state_entity in sensors.items():
                if self.entity_exists(conf_ha_sensor, namespace = self.hass_namespace): #confirm the entity still in HA and if not remove it
                    self.listen_state(self.confidence_updated, conf_ha_sensor, user_state_entity = user_state_entity, namespace = self.hass_namespace)
                else:
                    remove_sensors.append(conf_ha_sensor)

            if remove_sensors != []:
                for sensor in remove_sensors:
                    sensors.pop(sensor)

            db[self.presence_topic] = json.dumps(sensors) #reload new data
        
    def presence_message(self, event_name, data, kwargs):
        topic = data['topic']
        if topic.split('/')[0] != self.presence_topic: #only interested in the presence topics
            return

        payload = json.loads(data['payload'])

        location_Id = topic.split('/')[2]
        location = location_Id.replace('_', ' ').title()
        device_name = payload['name']
        user_name = payload['name'].lower().replace('’', '').replace(' ', '_')
        mac_address = topic.split('/')[3]
        confidence = int(payload['confidence'])
        user_conf_entity = '{}_{}'.format(user_name, location_Id)
        conf_ha_sensor = 'sensor.{}'.format(user_conf_entity)
        user_state_entity = '{}_home_state'.format(user_name)
        user_sensor = 'binary_sensor.{}'.format(user_state_entity)
        if user_state_entity not in self.not_home_timers: 
            self.not_home_timers[user_state_entity] = None #used to store the handle for the timer

        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)

        if not self.entity_exists(conf_ha_sensor, namespace = self.hass_namespace): #meaning it doesn't exist
            self.log('Creating sensor {!r} for Confidence'.format(conf_ha_sensor), level='INFO')
            topic =  'homeassistant/sensor/{}/{}/config'.format(self.presence_topic, user_conf_entity)
            payload = {"name": "{} {}".format(device_name, location), 
                        "state_topic":"homeassistant/sensor/{}/{}/state".format(self.presence_topic, user_conf_entity)}
            self.mqtt_send(topic, json.dumps(payload)) #send to homeassistant to create sensor for confidence

            '''create user home state sensor'''
            if not self.entity_exists(user_sensor, namespace = self.hass_namespace): #meaning it doesn't exist.
                                                                                    # it could be assumed it doesn't exist anyway
                self.log('Creating sensor {!r} for Home State'.format(user_sensor), level='INFO')
                topic =  'homeassistant/binary_sensor/{}/config'.format(user_state_entity)
                payload = {"name": "{} Home State".format(device_name), "device_class" : "presence",
                            "state_topic":"homeassistant/binary_sensor/{}/state".format(user_state_entity)}
                self.mqtt_send(topic, json.dumps(payload)) #send to homeassistant to create binary sensor sensor for home state

                '''create app states for mapping user sensors to confidence to store and can be picked up by other apps if needed'''
                if not self.entity_exists(appdaemon_entity):
                    self.set_app_state(appdaemon_entity, state = 'Initializing', attributes = {'Confidence Sensors': {conf_ha_sensor: confidence}, 'MAC Address' : [mac_address]})
            else:
                user_attributes = self.get_state(appdaemon_entity, attribute = 'all')['attributes']
                user_attributes['Confidence Sensors'].update({conf_ha_sensor: confidence})
                if mac_address not in user_attributes['MAC Address']:
                    user_attributes['MAC Address'].append(mac_address)
                self.set_app_state(appdaemon_entity, state = 'Updated', attributes = user_attributes)

            self.listen_state(self.confidence_updated, conf_ha_sensor, user_state_entity = user_state_entity, namespace = self.hass_namespace)

            with shelve.open(self.db_file) as db: #store sensors
                try:
                    sensors = json.loads(db[self.presence_topic])
                except:
                    sensors = {}
                sensors[conf_ha_sensor] = user_state_entity
                db[self.presence_topic] = json.dumps(sensors)
            

        else:
            sensor_reading = self.get_state(conf_ha_sensor, namespace = self.hass_namespace)
            if sensor_reading == 'unknown': #this will happen if HA was to restart
                sensor_reading = 0
            if int(sensor_reading) != confidence: 
                topic = "homeassistant/sensor/{}/{}/state".format(self.presence_topic, user_conf_entity)
                payload = confidence
                self.mqtt_send(topic, payload) #send to homeassistant to update sensor for confidence

                if not self.entity_exists(appdaemon_entity): #in the event AppD restarts and not HA
                    self.set_app_state(appdaemon_entity, state = 'Initializing', attributes = {'Confidence Sensors': {}, 'MAC Address' : []})
                user_attributes = self.get_state(appdaemon_entity, attribute = 'all')['attributes']
                user_attributes['Confidence Sensors'].update({conf_ha_sensor: confidence})
                if mac_address not in user_attributes['MAC Address']:
                    user_attributes['MAC Address'].append(mac_address)
                self.set_app_state(appdaemon_entity, state = 'Updated', attributes = user_attributes)


    def confidence_updated(self, entity, attribute, old, new, kwargs):
        confidence = new
        user_state_entity = kwargs['user_state_entity']
        user_sensor = 'binary_sensor.' + user_state_entity
        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)
        user_conf_sensors = self.get_state(appdaemon_entity, attribute = 'all')['attributes']['Confidence Sensors']
        if max(user_conf_sensors.values()) >= self.minimum_conf: #meaning at least one of them states is greater than the minimum so person definitely home
            if self.not_home_timers[user_state_entity] != None: #cancel timer if running
                self.cancel_timer(self.not_home_timers[user_state_entity])
                self.not_home_timers[user_state_entity] = None

            topic = "homeassistant/binary_sensor/{}/state".format(user_state_entity)
            payload = 'ON'
            self.mqtt_send(topic, payload) #send to homeassistant to update sensor that user home
            self.set_app_state(appdaemon_entity, state = 'Home') #not needed but one may as well for other apps

        else:
            if self.not_home_timers[user_state_entity] == None: #run the timer
                self.not_home_timers[user_state_entity] = self.run_in(self.not_home_func, self.timeout, user_state_entity = user_state_entity)

    def not_home_func(self, kwargs):
        user_state_entity = kwargs['user_state_entity']
        user_sensor = 'binary_sensor.' + user_state_entity
        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)
        user_conf_sensors = self.get_state(appdaemon_entity, attribute = 'all')['attributes']['Confidence Sensors']
        self.not_home_timers[user_state_entity] = None
        if max(user_conf_sensors.values()) < self.minimum_conf: #still confirm for the last time
            topic = "homeassistant/binary_sensor/{}/state".format(user_state_entity)
            payload = 'OFF'
            self.mqtt_send(topic, payload) #send to homeassistant to update sensor that user home
            self.set_app_state(appdaemon_entity, state = 'Not Home') #not needed but one may as well for other apps        

As stated above, all that is required is to make the changes to the owner_devices and guest_devices files, and AppD will populate the rest.

This is what AppD does

  1. Create sensors in HA based on each device per location sensor.<device name>_location (like in your case, you said you got 3). So each device will basically have 3 of these sensors and it multiplies like that
  2. These sensors are used to keep the confidence reading from the presence sensor.
  3. A binary sensor is then created in HA, per device binary_sensor.<device name>_home_state. So regardless of which location it is sensed, only one is created. So even though you have 3 presence system running, only a single binary sensor is created for each device. This is a presence sensor, so on HA it will show as presence (home/away).
  4. Based on the reading of each of the confidence sensors, AppD checks across all of them if anyone is more than the configured minimum_confidence. if any, it will set the binary sensor to home for that device
  5. If the user is no detected after the not_home_timeout, the binary sensor is then set for not home
  6. The created sensors are stored in a database as set in the apps.yaml file, so if there is a restart, it will reload the listen_state() functions
  7. If for any reason a sensor no longer exist (HA restarts, and maybe the user no longer around like a guest), AppD will remove the user from the database on next restart

To Do:
As stated earlier, modify app so a single user can be detected from multiple devices at the same time.

It is possible to do all these without involving HA, but as @ReneTode once commented, it is actually a good thing to have history of these detection and so necessary to have it added.

Hope this helps, and to be honest, I kind of like the speed of detection and away mode. The script really cool.

Regards

2 Likes

Hello @PianSom,

When I wrote this app, I hadn’t realised that @andrewjfreyer had two versions of the presence system. The presence.sh and monitor.sh. Over the weekend, I migrated to the monitor.sh, and due to how different its message structure is, it broke the app above. I have rewritten the app to function with the monitor.sh version, and it is below.

I must say one very good thing about the monitor.sh version is that its easier to anticipate a few things. Like in the config, if I name a particular MAC, its easy to tell what sensors will be generated for it, instead of seeking out device name. Like if I write in known_static_addresses xx:xx:xx:xx:xx:xx Odianosen's Iphone, I can easily expect sensors called sensor.odianosens_iphone_living_room for confidence (that is if my presence system is in the Living Room) and binary_sensor.odianosens_iphone_home_state for if the device is in the house.

Based on the above, automation can then be built in anticipation of these. Think about those running stuffs like AirBnB or if one was to have guests around and need some special automation for them in anticipation. I have also made the system a bit more reliant on the data generated in HA, instead of basing it on what is stored in AppD.

Lastly I do understand that some might prefer to be able to use this app, without needing to bring in the mqtt plugin as you mentioned earlier. If that is the case, there is a way to still achieve it using events. But as I don’t use it, working on it will be a well difficult and maintaining it will be a drag. I can assist if you need it, and as usual just holla.

import appdaemon.plugins.mqtt.mqttapi as mqtt
import json
import shelve

class HomePresenceApp(mqtt.Mqtt):

    def initialize(self):
        self.set_namespace('mqtt')
        self.hass_namespace = self.args.get('hass_namespace', 'default')
        self.presence_topic = self.args.get('presence_topic', 'presence')
        self.db_file = self.args['database']
        self.listen_event(self.presence_message, 'MQTT')
        self.not_home_timers = dict()
        self.timeout = self.args.get('not_home_timeout', 120) #time interval before declaring not home
        self.minimum_conf = self.args.get('minimum_confidence', 90)
        self.home_state_entities = dict()

        with shelve.open(self.db_file) as db: #check sensors
            remove_sensors = []

            try:
                sensors = db[self.presence_topic]
            except:
                sensors = {}

            if isinstance(sensors, str):
                sensors = json.loads(db[self.presence_topic])

            for conf_ha_sensor, user_state_entity in sensors.items():
                '''confirm the entity still in HA if not remove it'''
                if self.entity_exists(conf_ha_sensor, namespace = self.hass_namespace):
                    self.listen_state(self.confidence_updated, conf_ha_sensor, user_state_entity = user_state_entity, namespace = self.hass_namespace)
                else:
                    remove_sensors.append(conf_ha_sensor)

                if user_state_entity  not in self.home_state_entities:
                    self.home_state_entities[user_state_entity ] = list()

                if conf_ha_sensor not in remove_sensors and self.get_state(conf_ha_sensor, namespace = self.hass_namespace) != 'unknown': #meaning its not been scheduled to be removed
                    self.home_state_entities[user_state_entity].append(conf_ha_sensor)

            if remove_sensors != []:
                for sensor in remove_sensors:
                    sensors.pop(sensor)

            db[self.presence_topic] = json.dumps(sensors) #reload new data
        
    def presence_message(self, event_name, data, kwargs):
        topic = data['topic']
        if topic.split('/')[0] != self.presence_topic: #only interested in the presence topics
            return 

        elif topic.split('/')[1] != 'owner':
            return

        payload = json.loads(data['payload'])

        if payload.get('status', None) != None: #meaning its a message on the presence system
            location = topic.split('/')[2].replace('_',' ').title()
            self.log('The Presence System in the {} is {}'.format(location, payload.get('status').title()))

        if payload.get('type', None) != 'Known Static MAC' or payload.get('name', None) == 'Unknown Name': #confirm its for a known MAC address
            return

        location = topic.split('/')[2].replace('_',' ').title()
        location_Id = location.replace(' ', '_').lower()
        device_name = payload['name']
        user_name = payload['name'].lower().replace('’', '').replace(' ', '_').replace("'", "")
        mac_address = topic.split('/')[3]
        confidence = int(payload['confidence'])
        user_conf_entity = '{}_{}'.format(user_name, location_Id)
        conf_ha_sensor = 'sensor.{}'.format(user_conf_entity)
        user_state_entity = '{}_home_state'.format(user_name)
        user_sensor = 'binary_sensor.{}'.format(user_state_entity)
        if user_state_entity not in self.not_home_timers: 
            self.not_home_timers[user_state_entity] = None #used to store the handle for the timer

        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)

        if not self.entity_exists(conf_ha_sensor, namespace = self.hass_namespace): #meaning it doesn't exist
            self.log('Creating sensor {!r} for Confidence'.format(conf_ha_sensor), level='INFO')
            topic =  'homeassistant/sensor/{}/{}/config'.format(self.presence_topic, user_conf_entity)
            state_topic = "homeassistant/sensor/{}/{}/state".format(self.presence_topic, user_conf_entity)
            payload = {"name": "{} {}".format(device_name, location), "state_topic": state_topic}
            self.mqtt_send(topic, json.dumps(payload)) #send to homeassistant to create sensor for confidence

            '''create user home state sensor'''
            if not self.entity_exists(user_sensor, namespace = self.hass_namespace): #meaning it doesn't exist.
                                                                                    # it could be assumed it doesn't exist anyway
                self.log('Creating sensor {!r} for Home State'.format(user_sensor), level='INFO')
                topic =  'homeassistant/binary_sensor/{}/config'.format(user_state_entity)
                payload = {"name": "{} Home State".format(device_name), "device_class" : "presence", 
                            "state_topic": "homeassistant/binary_sensor/{}/state".format(user_state_entity)}
                self.mqtt_send(topic, json.dumps(payload)) #send to homeassistant to create binary sensor sensor for home state

                '''create app states for mapping user sensors to confidence to store and can be picked up by other apps if needed'''
                if not self.entity_exists(appdaemon_entity):
                    self.set_app_state(appdaemon_entity, state = 'Initializing', attributes = {'Confidence' : confidence})

            else:
                user_attributes = self.get_state(appdaemon_entity, attribute = 'all')['attributes']
                user_attributes['Confidence'] = confidence
                self.set_app_state(appdaemon_entity, state = 'Updated', attributes = user_attributes)

            self.listen_state(self.confidence_updated, conf_ha_sensor, user_state_entity = user_state_entity, namespace = self.hass_namespace)

            if user_state_entity not in self.home_state_entities:
                self.home_state_entities[user_state_entity] = list()

            if conf_ha_sensor not in self.home_state_entities[user_state_entity]: #not really needed, but noting wrong in being extra careful
                self.home_state_entities[user_state_entity].append(conf_ha_sensor)

            self.run_in(self.update_sensor, 3, topic = state_topic, payload = confidence) #use delay so HA has time to setup sensor first before updating

            with shelve.open(self.db_file) as db: #store sensors
                try:
                    sensors = json.loads(db[self.presence_topic])
                except:
                    sensors = {}
                sensors[conf_ha_sensor] = user_state_entity
                db[self.presence_topic] = json.dumps(sensors)

        else:
            if user_state_entity not in self.home_state_entities:
                self.home_state_entities[user_state_entity] = list()

            if conf_ha_sensor not in self.home_state_entities[user_state_entity]:
                self.home_state_entities[user_state_entity].append(conf_ha_sensor)
                self.listen_state(self.confidence_updated, conf_ha_sensor, user_state_entity = user_state_entity, namespace = self.hass_namespace)
                with shelve.open(self.db_file) as db: #store sensors
                    try:
                        sensors = json.loads(db[self.presence_topic])
                    except:
                        sensors = {}
                    sensors[conf_ha_sensor] = user_state_entity
                    db[self.presence_topic] = json.dumps(sensors)
                
            sensor_reading = self.get_state(conf_ha_sensor, namespace = self.hass_namespace)
            if sensor_reading == 'unknown': #this will happen if HA was to restart
                sensor_reading = 0
            if int(sensor_reading) != confidence: 
                topic = "homeassistant/sensor/{}/{}/state".format(self.presence_topic, user_conf_entity)
                payload = confidence
                self.mqtt_send(topic, payload) #send to homeassistant to update sensor for confidence

                if not self.entity_exists(appdaemon_entity): #in the event AppD restarts and not HA
                    self.set_app_state(appdaemon_entity, state = 'Initializing', attributes = {'Confidence' : confidence})
                user_attributes = self.get_state(appdaemon_entity, attribute = 'all')['attributes']
                user_attributes['Confidence'] = confidence
                self.set_app_state(appdaemon_entity, state = 'Updated', attributes = user_attributes)


    def confidence_updated(self, entity, attribute, old, new, kwargs):
        user_state_entity = kwargs['user_state_entity']
        user_sensor = 'binary_sensor.' + user_state_entity
        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)
        user_conf_sensors = self.home_state_entities.get(user_state_entity, None)
        #self.log('__function__, __line__, user_conf_sensors: {}'.format(user_conf_sensors))
        if user_conf_sensors != None:
            sensor_res = list(map(lambda x: self.get_state(x, namespace = self.hass_namespace), user_conf_sensors))
            sensor_res = [i for i in sensor_res if i != 'unknown'] # remove unknown vales from list
            if  sensor_res != [] and any(list(map(lambda x: int(x) >= self.minimum_conf, sensor_res))): #meaning at least one of them states is greater than the minimum so device definitely home
                if self.not_home_timers[user_state_entity] != None: #cancel timer if running
                    self.cancel_timer(self.not_home_timers[user_state_entity])
                    self.not_home_timers[user_state_entity] = None

                topic = "homeassistant/binary_sensor/{}/state".format(user_state_entity)
                payload = 'ON'
                self.mqtt_send(topic, payload) #send to homeassistant to update sensor that user home
                self.set_app_state(appdaemon_entity, state = 'Home') #not needed but one may as well for other apps

            else:
                if self.not_home_timers[user_state_entity] == None and self.get_state(user_sensor, namespace = self.hass_namespace) != 'off': #run the timer
                    self.not_home_timers[user_state_entity] = self.run_in(self.not_home_func, self.timeout, user_state_entity = user_state_entity)

    def not_home_func(self, kwargs):
        user_state_entity = kwargs['user_state_entity']
        user_sensor = 'binary_sensor.' + user_state_entity
        appdaemon_entity = '{}.{}'.format(self.presence_topic, user_state_entity)
        user_conf_sensors = self.home_state_entities[user_state_entity]
        sensor_res = list(map(lambda x: self.get_state(x, namespace = self.hass_namespace), user_conf_sensors))
        sensor_res = [i for i in sensor_res if i != 'unknown'] # remove unknown vales from list
        if  all(list(map(lambda x: int(x) < self.minimum_conf, sensor_res))): #still confirm for the last time
            topic = "homeassistant/binary_sensor/{}/state".format(user_state_entity)
            payload = 'OFF'
            self.mqtt_send(topic, payload) #send to homeassistant to update sensor that user home
            self.set_app_state(appdaemon_entity, state = 'Not Home') #not needed but one may as well for other apps    

    def update_sensor(self, kwargs):
        topic = kwargs['topic']
        payload = kwargs['payload']
        self.mqtt_send(topic, payload) #send to homeassistant to update sensor for confidence
1 Like