openhab-scripters / openhab-helper-libraries

Scripts and modules for use with openHAB
Eclipse Public License 1.0
88 stars 69 forks source link

Generic Presence Detection #234

Closed rkoshak closed 3 years ago

rkoshak commented 4 years ago

Initial implementation of the Generic Presence Detection example from the forum. I've expanded it somewhat so that it can track both overall presence as well as presence of individuals. I use Scott's automatically generated Rule Triggers trick to make that happen. Configuration takes place through three variables in configuration.py, two lists (list of sensor Items and corresponding proxy Items) as well as the flapping time.

As I type this message it occurs to me that I could use a single List of tuples which would let me have a different flapping timeout value for each person. I'll await the initial review though before I add that.

rkoshak commented 4 years ago

I was experiencing some strange behavior (i.e. Timers not being cancelled when they are supposed to be) so I simplified some of the code and and added a new Rule to synchronize the proxy Items with the current/restored sensor readings at Rule startup. I also added code where the user can set a name metadata on the proxy Item and get more human friendly logs and/or use it in TTS announcements and the like.

volfan6415 commented 4 years ago

Rather than configured with a list of items could the script be configured to use a group that is defined?

rkoshak commented 4 years ago

It already works that way. The list is expected to be a list of Groups. In my current setup and in the example in the docstrings, there are three Groups. One generic group that represents when someone is home (gPresent), one specific group that represents when Rich is home (gRichPresent) and one group that represents when Jenn (gJennPresent) is home. Each of those Groups have more than one member and the script will track each of them separately.

If you only care whether someone is home, you just need the one Group (gPresent). The docs are to show how you can also separately track individual people, not just over all presence.

boehan commented 4 years ago

@rkoshak is the most recent version in this PR actually working for you? For me there seems to be anything wrong with the lambda function, which I can't get fixed. I always get the following errors when the rule is triggered:

2019-11-09 16:02:29.024 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.Timer 3 2019-11-09T16:02:29.011+01:00: <function <lambda> at 0x5> threw an unhandled Exception: 
org.python.core.PyException: null
    at org.python.core.Py.NameError(Py.java:290) ~[?:?]
    at org.python.core.PyFrame.getglobal(PyFrame.java:265) ~[?:?]
    at org.python.pycode._pyx33.get_name$1(<script>:78) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:139) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:413) ~[?:?]
    at org.python.pycode._pyx33.away$6(<script>:127) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:308) ~[?:?]
    at org.python.core.PyFunction.function___call__(PyFunction.java:471) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:466) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:456) ~[?:?]
    at org.python.pycode._pyx33.f$5(<script>:106) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:125) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:403) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:398) ~[?:?]
    at org.python.core.PyFunction.invoke(PyFunction.java:533) ~[?:?]
    at com.sun.proxy.$Proxy367.apply(Unknown Source) ~[?:?]
    at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [182:org.openhab.core.scheduler:2.5.0.M4]
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [182:org.openhab.core.scheduler:2.5.0.M4]
2019-11-09 16:02:29.078 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.Timer 3 2019-11-09T16:02:29.011+01:00: <function <lambda> at 0x5> threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
    at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [182:org.openhab.core.scheduler:2.5.0.M4]
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [182:org.openhab.core.scheduler:2.5.0.M4]
Caused by: org.python.core.PyException
    at org.python.core.Py.NameError(Py.java:290) ~[?:?]
    at org.python.core.PyFrame.getglobal(PyFrame.java:265) ~[?:?]
    at org.python.pycode._pyx33.get_name$1(<script>:78) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:139) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:413) ~[?:?]
    at org.python.pycode._pyx33.away$6(<script>:127) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:308) ~[?:?]
    at org.python.core.PyFunction.function___call__(PyFunction.java:471) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:466) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:456) ~[?:?]
    at org.python.pycode._pyx33.f$5(<script>:106) ~[?:?]
    at org.python.pycode._pyx33.call_function(<script>) ~[?:?]
    at org.python.core.PyTableCode.call(PyTableCode.java:171) ~[?:?]
    at org.python.core.PyBaseCode.call(PyBaseCode.java:125) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:403) ~[?:?]
    at org.python.core.PyFunction.__call__(PyFunction.java:398) ~[?:?]
    at org.python.core.PyFunction.invoke(PyFunction.java:533) ~[?:?]
    at com.sun.proxy.$Proxy367.apply(Unknown Source) ~[?:?]
    at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:48) ~[?:?]
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
    ... 1 more

any ideas?

rkoshak commented 4 years ago

I've been running with it since before I posted it here. The lambda itself is as simple as can be. I'm not sure what could be throwing an exception from there. Put everything in away() inside a try/except and log out the contents of the exception.

def away(log, events, proxy_name, item_name, flap_time):
    """
    Called when a person has been detected away for more than the flapping time.
    Log out the away state and command the Item to OFF.
    Arguments:
        - log: Logger passed in from the presence Rule.
        - events: Access to the events Object so we can call sendCommand.
        - proxy_name: Name of the proxy Item that we want to set to away.
        - item_name: Name of the sensor Item that detects presence.
        - flap_time: Number of minutes we wait before marking the person away.
    """
    try:
        if items[proxy_name] == OFF:
            log.warn("Presence away timer expired but {} is already OFF!"
                     .format(proxy_name))
        elif items[item_name] == ON:
            log.warn("Presence away timer expired but {} is ON!".format(item_name))
        else:
            log.info("{} has been away for {} minutes, setting to away."
                    .format(get_name(proxy_name),flap_time))
            events.sendCommand(proxy_name, "OFF")
    except:
        import sys
        log.error("Error occurred in away: {}".format(sys.exec_info()[0]))

That should tell us what the error is and on which line it occurs. I can't imagine what it would be though. Perhaps proxy_name is None instead of the name of the proxy Item in which case there may be something wrong with your configuration.py settings.

rkoshak commented 4 years ago

This PR is ready for review

boehan commented 4 years ago

I've been running with it since before I posted it here. The lambda itself is as simple as can be. I'm not sure what could be throwing an exception from there. Put everything in away() inside a try/except and log out the contents of the exception.

def away(log, events, proxy_name, item_name, flap_time):
    """
    Called when a person has been detected away for more than the flapping time.
    Log out the away state and command the Item to OFF.
    Arguments:
        - log: Logger passed in from the presence Rule.
        - events: Access to the events Object so we can call sendCommand.
        - proxy_name: Name of the proxy Item that we want to set to away.
        - item_name: Name of the sensor Item that detects presence.
        - flap_time: Number of minutes we wait before marking the person away.
    """
    try:
        if items[proxy_name] == OFF:
            log.warn("Presence away timer expired but {} is already OFF!"
                     .format(proxy_name))
        elif items[item_name] == ON:
            log.warn("Presence away timer expired but {} is ON!".format(item_name))
        else:
            log.info("{} has been away for {} minutes, setting to away."
                    .format(get_name(proxy_name),flap_time))
            events.sendCommand(proxy_name, "OFF")
    except:
        import sys
        log.error("Error occurred in away: {}".format(sys.exec_info()[0]))

That should tell us what the error is and on which line it occurs. I can't imagine what it would be though. Perhaps proxy_name is None instead of the name of the proxy Item in which case there may be something wrong with your configuration.py settings.

That did not help, but I think I found the problem. After adding from core.metadata import get_value to the imports it seems to be working now (will have to test this over the next days).

rkoshak commented 4 years ago

I'm not certain why that import is missing in the first place, but that would cause problems.
When I look at my copy it has the import but the import is clearly missing here. I messed up somewhere.

EDIT: I talked with Scott and got permission to update this PR.

I also figured out what went wrong. In my environment I have this code in two places (I have a utility function to get_name using get_value). When I copied it over and tested it I didn't run through the get_name code apparently.

rkoshak commented 3 years ago

I've basically completely rewritten this so am closing this PR. When reviewing submissions to the helper library becomes a priority I'll resubmit. In the mean time I'll keep in in my own repo where I can continue development.