[SOLVED] Monitoring for a state unchnaged

Ok I promise I’m not trying to bomb out the forum today.

So problem #2 for me today. While researching how to track an unchanged state I found this example here

which I took and set it up for what I needed it to do as

import appdaemon.appapi as appapi
import homeassistant.util.dt as dt_util
import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
# notify = comma separated list of notifiers
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):
    time = datetime.time(0, 0, 0)
    self.run_minutely(self.check_sensor, time)

  def check_sensor(self, kwargs):
    now = dt_util.now()
    sensor_last_updated_str = self.get_state(self.args["sensorID"], "last_updated")
    sensor_last_updated = dt_util.parse_datetime(sensor_last_updated_str)

    minutes = int(self.args["minutes"])

    stale_time = now - sensor_last_updated
    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      self.run_in(self.switch_on_function, 5)

  def light_off(self,kwargs):
    self.turn_on(self.args["switchID"])

I have an aeon lab energy meter that occasionally throughout the day just decides it no longer wants to report so the fix is I stuck a leviton zwave plug module on it and I turn that off and back on. In ST I was monitoring the value for it to remain the same over 4 mins. Problem is that I get a lot of false positives where the energy usage value has changed but the reading from 4 min ago vs now is actually the same. So the fix is turn it off for 5 seconds and then back on.

My hope is that with appdaemon I can do this a little more graceful and this code example looked like it would do the trick. But HA complains about this

import homeassistant.util.dt as dt_util

I’m guessing that either HASSIO doesn’t know how to import that or its been deprecated with HASSIO and there is possibly another library that I can utilize instead to accomplish what this library was accomplishing. Thank you for your help with this and if there is a better api reference for a value unchanged and I just can’t find it in the docs please feel free to slap me around, as I am def not trying to waste ones time with simple things although I’m sure that others might have a need for monitoring a value that isn’t changing.

are you using appdaemon2 or 3?
this app is written for appdaemon 2 (if you run 3, you need to change the lines with appapi)

that said, i can also say that you dont need dt_util in this case.

now = self.datetime() will work also.

BTW I noticed some mistakes I made so I fixed those and I’m currently researching the dt_util stuff as I think its date/time related and I’m conforming it to those standards.

EDIT: @ReneTode I’m using v2 of appdaemon and good to know on the now reference. I saw you were on the thread where I found that original sample from and that you were also attempting to solve for this.

i wouldnt check for last updated i would use an extra sensor nowadays.

init():
  self.listen_state(self.updatetime,self.args["sensorID"])

def updatetime(...):
  self.set_state("sensor.anything_last_updated", state = self.datetime())

that way you are sure you have a real last update, it will be visible in ha and you can use a get_state to find the last time the sensor is updated.

1 Like

Ok so this is what I’ve come up with

import appdaemon.appapi as appapi
from datetime import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):

    self.listen_state(self.updatetime,self.args["sensorID"])


  def updatetime(self, entity, attribute, old, new, kwargs):
    self.set_state("sensor.last_updated", state = self.datetime())

    minutes = int(self.args["minutes"])

    now = self.datetime()

    stale_time = now - sensor_last_updated

    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      time.sleep(5)
      self.turn_on(self.args["switchID"])

But it says

2018-03-05 15:44:12.047947 WARNING ------------------------------------------------------------
2018-03-05 15:44:12.051905 WARNING Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/appdaemon/appdaemon.py", line 525, in worker
    ha.sanitize_state_kwargs(args["kwargs"]))
  File "/config/hadaemon/apps/stale_sensor.py", line 21, in updatetime
    self.set_state("sensor.last_updated", state = self.datetime())
  File "/usr/lib/python3.6/site-packages/appdaemon/appapi.py", line 24, in func_wrapper
    return(func(*args, **kwargs))
  File "/usr/lib/python3.6/site-packages/appdaemon/appapi.py", line 236, in set_state
    verify=conf.certpath)
  File "/usr/lib/python3.6/site-packages/requests/api.py", line 112, in post
    return request('post', url, data=data, json=json, **kwargs)
  File "/usr/lib/python3.6/site-packages/requests/api.py", line 58, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/lib/python3.6/site-packages/requests/sessions.py", line 494, in request
    prep = self.prepare_request(req)
  File "/usr/lib/python3.6/site-packages/requests/sessions.py", line 437, in prepare_request
    hooks=merge_hooks(request.hooks, self.hooks),
  File "/usr/lib/python3.6/site-packages/requests/models.py", line 308, in prepare
    self.prepare_body(data, files, json)
  File "/usr/lib/python3.6/site-packages/requests/models.py", line 458, in prepare_body
    body = complexjson.dumps(json)
  File "/usr/lib/python3.6/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "/usr/lib/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python3.6/json/encoder.py", line 180, in default
    o.__class__.__name__)
TypeError: Object of type 'datetime' is not JSON serializable

which tells me that something is very bad with how I’m doing something with the datetime.

yeah, sorry my bad.

you cant set a datetime, you need to create an str from it.
and how you now have it wont work.
because its only checking if the sensor is updated.

import appdaemon.appapi as appapi
import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
# notify = comma separated list of notifiers
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):
    time = datetime.time(0, 0, 0)
    self.run_minutely(self.check_sensor, time)
    self.listen_state(self.updatetime,self.args["sensorID"])

  def updatetime(self, entity, attribute, old, new, kwargs):
    actualtime = datetime.datetime.now()    
    actualtimestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    self.set_state("sensor.last_updated", state = actualtime)
  
  def check_sensor(self, kwargs):
    now = datetime.datetime.now() 
    sensor_last_updated_str = self.get_state(self.args["sensorID"], "last_updated")
    sensor_last_updated =datetime.datetime.strptime(sensor_last_updated_str,"%Y-%m-%d %H:%M:%S")

    minutes = int(self.args["minutes"])

    stale_time = now - sensor_last_updated
    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      self.run_in(self.switch_on_function, 5)

  def light_off(self,kwargs):
    self.turn_on(self.args["switchID"])

Thank you so much for your help with this. I’m loading it up now to see how it does. I promise as soon as I am fluent with more than just the basics here I will help others :slight_smile:

1 Like

Its still complaining about the json

import appdaemon.appapi as appapi
import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):
    time = datetime.time(0, 0, 0)
    self.run_minutely(self.check_sensor, time)
    self.listen_state(self.updatetime,self.args["sensorID"])

  def updatetime(self, entity, attribute, old, new, kwargs):
    actualtime = datetime.datetime.now()    
    actualtimestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    self.set_state("sensor.last_updated", state = actualtime)
  
  def check_sensor(self, kwargs):
    now = datetime.datetime.now() 
    sensor_last_updated_str = self.get_state(self.args["sensorID"], "last_updated")
    sensor_last_updated = datetime.datetime.strptime(sensor_last_updated_str,"%Y-%m-%d %H:%M:%S")

    minutes = int(self.args["minutes"])

    stale_time = now - sensor_last_updated
    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      self.run_in(self.switch_on_function, 5)

  def switch_on_function(self,kwargs):
    self.turn_on(self.args["switchID"])

I fixed a few things from your post to better match it.

EDIT: For learning purposes because I’m doing the run in section so quick is it better to just put a sleep there or actually have appdaemon resched it for 5 sec later?

sorry reading back i see another error.

    sensor_last_updated_str = self.get_state(self.args["sensorID"], "last_updated")

should be:

    sensor_last_updated_str = self.get_state("sensor.last_updated")

and its wise to chose another name for “last_updated”
that sensor is a last updated time for the sensor you use
i use device, entity = self.split_entity(entity_id) and then use entity in the name like:

    device, entity_name = self.split_entity(self.args["sensorID"])
    self.set_state("sensor." + entity_name + "last_updated", state = actualtime)

and

    device, entity_name = self.split_entity(self.args["sensorID"])
    sensor_last_updated_str = self.get_state("sensor." + entity_name + "last_updated")

the run_in is preffered.
a sleep holds the python thread from appdaemon, a run_in releases it.
if you use lots of sleeps (in stead of run_ins) your appdaemon could get slowed down.

Yeah I wondered if that was what was going on. I’ll go edit one of my other scripts because I have a sleep in there 2 hours so I’ll change that to a run_in so it doesn’t tie it up.

That being said hows this look now?

import appdaemon.appapi as appapi
import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):
    time = datetime.time(0, 0, 0)
    self.run_minutely(self.check_sensor, time)
    self.listen_state(self.updatetime,self.args["sensorID"])

  def updatetime(self, entity, attribute, old, new, kwargs):
    actualtime = datetime.datetime.now()    
    actualtimestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    self.set_state("sensor." + entity_name + "last_updated", state = actualtime)
  
  def check_sensor(self, kwargs):
    now = datetime.datetime.now() 
    sensor_last_updated_str = self.get_state("sensor." + entity_name + "last_updated")
    sensor_last_updated = datetime.datetime.strptime(sensor_last_updated_str,"%Y-%m-%d %H:%M:%S")

    minutes = int(self.args["minutes"])

    stale_time = now - sensor_last_updated
    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      self.run_in(self.switch_on_function, 5)

  def switch_on_function(self,kwargs):
    self.turn_on(self.args["switchID"])

another error in there (again my bad)
you should use the actualtimestr there

edit: of course also in your new line :wink:

and you can change the visualisation from the date and time to your wish.

“%Y-%m-%d %H:%M:%S”
this string represents how its shown in home assistant.
you cn change that to

"%Y\%m\%d %H:%M:%S" or
"%d-%m-%Y %H:%M:%S"

for example

(dont forget to keep both strings the same in the get_state and set_state section)

Ok here we go with the final result I think for anyone else who wants to do this as well.

This app is for monitoring something i.e. a sensor. You want to monitor it to see if the sensor has updated in X minuets. Thank you @ReneTode for your wonderful assistance with this.

import appdaemon.appapi as appapi
import datetime

#
# Notify stale sensors.
# Each minute last update of sensor is checked to see if it is higher than the configured value
#
# Args:
# sensor = sensor to monitor
# minute = minutes threshold to notify
#

class StaleSensor(appapi.AppDaemon):

  def initialize(self):
    device, entity_name = self.split_entity(self.args["sensorID"])
    if not self.entity_exists("sensor." + entity_name + "_last_updated"):
      self.log("Our virtual sensor doesn't exist so we'll create it now to prevent false alerts")    
      actualtime = datetime.datetime.now()    
      actualtimestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
      self.set_state("sensor." + entity_name + "last_updated", state = actualtimestr)
    time = datetime.time(0, 0, 0)
    self.run_minutely(self.check_sensor, time)
    self.listen_state(self.updatetime,self.args["sensorID"])

  def updatetime(self, entity, attribute, old, new, kwargs):
    actualtime = datetime.datetime.now()    
    actualtimestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    device, entity_name = self.split_entity(self.args["sensorID"])
    self.set_state("sensor." + entity_name + "last_updated", state = actualtimestr)
  
  def check_sensor(self, kwargs):
    now = datetime.datetime.now() 
    device, entity_name = self.split_entity(self.args["sensorID"])
    sensor_last_updated_str = self.get_state("sensor." + entity_name + "last_updated")
    sensor_last_updated = datetime.datetime.strptime(sensor_last_updated_str,"%Y-%m-%d %H:%M:%S")

    minutes = int(self.args["minutes"])

    stale_time = now - sensor_last_updated
    if stale_time.seconds > minutes*60:
      self.log("The power meter hasn't updated in a while so we're going to restart it")
      self.turn_off(self.args["switchID"])
      self.run_in(self.switch_on_function, 5)

  def switch_on_function(self,kwargs):
    self.turn_on(self.args["switchID"])

For me I just turn off a switch for 5 seconds and then I turn it back on. You can of course take this and tailor it to do whatever you need it to do.

Edit: fixed the sensorID for the updated to be a little more dynamic per @ReneTode

Edit2: Added the lines to create our virtual sensor on first run of the app.

1 Like

but “sensorID” is also static.
thats why i gave you the split_entity option.
so you could create a sensor that is based on the sensor ID that you have in your args.

this will work if you use the app for just 1 device.
if you want to use it for several devices you need to create a sensor that is based on the device you are monitoring.

Dang I thought that was doing that. Should it be self.args[“sensorID”]?

device, entity_name = self.split_entity(self.args["sensorID"])
sensor_last_updated_str = self.get_state("sensor." + entity_name + "last_updated")

and the same with the set_state

Oh I see now, man this head cold I have is really screwing with me.

thats to bad for you, but no problem for me.
i am a patient man :wink:

1 Like

Ok so now it says that the new sensor doesn’t exist which I assume is where I need to go to my sensors file and create a virtual sensor of some sort for this?

The warning no error though

2018-03-05 17:14:12.073064 WARNING power_meter_check: Entity sensor.whole_house_energylast_updated not found in Home Assistant