Page 5 of 5

Re: Ring 2 Doorbell

Posted: Friday 13 January 2023 9:50
by Korrel
robgeerts wrote: Thursday 16 June 2022 23:23 I installed this script today and it almost works, the problem is when i actived motion detection.
"last_event_type" is in my case always 'motion'...
Never 'ding'.

When i disabled motion detection, my 'last_event_type' is 'ding' and the script is working fine.
In the most ideal situation i want one domoticz switch for motion and one for ding.. how to solve if ding is never a 'last_event_type'?

Code: Select all

def check_device_has_active_alerts(device_name: str, doorbells):
    try:
        triggered_device = next((doorbell for doorbell in doorbells if doorbell.name == device_name), None)
        last_event_type = triggered_device.history(limit=1)[0]['kind']
        newest_event_id = find_newest_recording_id(triggered_device)

        if "ding" in last_event_type:
            print(f"{ts()}: {device_name} '{last_event_type}' event was triggered (id: {newest_event_id})")
            notify_domoticz(device_name)
    except:
        triggered_device, last_event_type, newest_event_id = None, None, None

    return triggered_device, last_event_type, newest_event_id

The script is getting events from history. A ding is mostly after a motion... Since a motion last for 40-60s, this would be the last item being stored in history when the ding appears...
I've recoded the script to not look at history but take the event at that time.
Ive also changed notify domoticz to trigger different ding and motion devices ;)

Code: Select all

import json, os
from pathlib import Path
from ring_doorbell import Ring, Auth
from oauthlib.oauth2 import MissingTokenError
import time
from datetime import datetime, timezone
import urllib.request as urllib
from threading import Thread

######### This Section Needs Edited By You - unique_api_name as well! ###########################
your_username = '***@*****.*l'
your_password = **************'
your_unique_api_name = 'PythonMonitorAPI/0.1.1'
cache_file = Path("/home/pi/domoticz/scripts/ring/cached_auth_token.json")
videopath = '/mnt/documents/ring/captures/'
poll_interval = 1.0  # interval in seconds that we poll the Ring API for new events
# Domoticz setup
domoticz_api = 'http://192.168.1.210:8080'
domoticz_idx_mapping_ding = {"Voordeur": 6711, "Back Door": 6711}
domoticz_idx_mapping_motion = {"Voordeur": 11231, "Back Door": 11231}
#################################################################################################

###### DO NOT EDIT THESE IF YOU DO NOT KNOW WHAT THEY ARE FOR ###################################
# .env overrides: 1) create your .env file with the exported VARS below (ie. export RING_USERNAME='MYUSERNAME')
#                 2) source them and start this script like:  $ source .env && python3 <script_name>.py
# Note: decided to handle this natively rather than use the python-dotenv package
username = os.getenv('RING_USERNAME', your_username)
password = os.getenv('RING_PASSWORD', your_password)
unique_api_name = os.getenv('RING_API_NAME', your_unique_api_name)

def authenticate():
    if cache_file.is_file():
        auth = Auth(unique_api_name, json.loads(cache_file.read_text()), token_updated)
    else:
        auth = Auth(unique_api_name, None, token_updated)
        try:
            auth.fetch_token(username, password)
        except MissingTokenError:
            auth.fetch_token(username, password, otp_callback())

    auth = Ring(auth)
    return auth

def authenticate_and_initialize(gadget_type: str = "doorbells"):
    myring = authenticate()
    myring.update_data()
    devices = myring.devices()

    if gadget_type is "doorbells":
        doorbells = devices['doorbots']
        enumerate_doorbells(doorbells)

        return myring, doorbells

def ts(filename_format: bool = False, dirname_format: bool = False):
    if dirname_format:
        return f"{datetime.now().strftime('%Y-%m-%d')}/"
    if filename_format:
        return datetime.now().strftime('%H-%M-%S')
    else:
        return datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def construct_local_filename(triggered_device, device_name: str, last_event_type: str, newest_event_id: int):
    filename_timestamp = triggered_device.history(limit=1, enforce_limit=True)[0]['created_at'].replace(
        tzinfo=timezone.utc).astimezone(tz=None).strftime('%H-%M-%S')

    local_filename = f"{videopath}{daily_directory()}{device_name.replace(' ', '')}_{last_event_type}_" \
                     f"{filename_timestamp}_{str(newest_event_id)[-4:]}.mp4"

    return local_filename


def daily_directory():
    daily_directory_path = videopath + ts(dirname_format=True)

    if os.path.isdir(daily_directory_path):
        return ts(dirname_format=True)
    else:
        os.mkdir(daily_directory_path)
        print(f"{ts()}: Created new daily directory at {daily_directory_path}")
        return ts(dirname_format=True)


def token_updated(token):
    cache_file.write_text(json.dumps(token))


def otp_callback():
    auth_code = input("2FA code: ")
    return auth_code


def enumerate_doorbells(doorbells):
    for doorbell in doorbells:
        print(f"{ts()}: Authentication and discovery successful for {doorbell}.")


def notify_domoticz(device_name: str, this_event_type: str):
    try:
        if this_event_type == "ding":
          idx = domoticz_idx_mapping_ding.get(device_name)
        if this_event_type == "motion":
          idx = domoticz_idx_mapping_motion.get(device_name)
        urllib.urlopen(f'{domoticz_api}/json.htm?type=command&param=switchlight&idx={idx}&switchcmd=On')
        print(f"{ts()}: Domoticz (idx:{idx}) notified of event at {device_name}")
    except:
        print(f"{ts()}: Error switching to domoticz : '{domoticz_api}/json.htm?type=command&param=switchlight&idx={idx}&switchcmd=On'.")

def main():
    myring, doorbells = authenticate_and_initialize(gadget_type="doorbells")

    if myring and doorbells:
        print(f"{ts()}: Entering observation loop - polling interval set to {poll_interval}s\n")
    while True:

        myring.update_dings()
        alerts = myring.active_alerts()

        if alerts:
            for alert in alerts:
                try:
                    this_event_id = alert['id']
                    this_device_name = alert['doorbot_description']
                    this_event_type = alert['kind']
                    print(f"{ts()}: {this_device_name} '{this_event_type}' event was triggered (id: {this_event_id})")
                    if this_event_type in "dingmotion":
                      notify_domoticz(this_device_name, this_event_type)

                except:
                    time.sleep(poll_interval)
                    continue

                time.sleep(poll_interval)

if __name__ == "__main__":
    main()


Re: Ring 2 Doorbell

Posted: Friday 13 January 2023 9:56
by robgeerts
Nice, thanks Korrel, will test asap!

Re: Ring 2 Doorbell

Posted: Wednesday 22 February 2023 22:36
by pvdgulik
Hello, interested to get this working. Have to questions:

First, how to get my_own_unique_API_name: Followed this but still don't have a clue:
" 7) If you are getting missing_token errors, you must use your own unique API name and version (see this post: https://github.com/tchellomello/python- ... -874319220 ). Ring seems to be flagging the default value and blocking it."

Second, is it possible to run this script in Domoticz-scripts?

Thanks!

Re: Ring 2 Doorbell

Posted: Thursday 23 February 2023 7:28
by Egregius
First one is easy. "ThisCouldBeYourApiName1.0" or anything you can imagen.
Second one, no, it's not a domoticz script. It runs by it's own.

Re: Ring 2 Doorbell

Posted: Saturday 22 July 2023 22:06
by Jerby
Does anyone has the last script working? I still get only Motion events in Domoticz and no downloaded video's in the rPi directory.
What code is required to get a snapshot when Motion or Ding is detected?

Re: Ring 2 Doorbell

Posted: Thursday 04 April 2024 15:30
by Korrel
Jerby wrote: Saturday 22 July 2023 22:06 Does anyone has the last script working? I still get only Motion events in Domoticz and no downloaded video's in the rPi directory.
What code is required to get a snapshot when Motion or Ding is detected?
Several things have changed in the authorisation(-token) ....

See my code for a working ding implententation (please upgrade api to latest)

Code: Select all

import json, os
from pathlib import Path
from ring_doorbell import Ring, Auth
from oauthlib.oauth2 import MissingTokenError
import time
from datetime import datetime, timezone
import urllib.request as urllib
from threading import Thread

######### This Section Needs Edited By You - unique_api_name as well! ###########################
your_unique_api_name = 'PythonMonitorAPI/0.1.1'
user_agent = "Myproject_ringdoor_1.0"  # Change this
cache_file = Path("/home/pi/domoticz/scripts/ring/" + user_agent + ".token.cache")
videopath = '/mnt/documents/ring/captures/'
poll_interval = 1.0  # interval in seconds that we poll the Ring API for new events
# Domoticz setup
domoticz_api = 'http://192.168.x.x:8080'
domoticz_idx_mapping_ding = {"Voordeur": 6711, "Back Door": 6711, "Recording": 11492}
domoticz_idx_mapping_motion = {"Voordeur": 11231, "Back Door": 11231, "Recording": 11492}
#################################################################################################

def authenticate():
    if cache_file.is_file():
        auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated)
        myring = Ring(auth)
        try:
            myring.create_session()  # auth token still valid
        except AuthenticationError:  # auth token has expired
            auth = do_auth()
    else:
        auth = do_auth()  # Get new auth token
    auth = Ring(auth)
    return auth

def authenticate_and_initialize(gadget_type: str = "doorbells"):
    myring = authenticate()
    myring.update_data()
    devices = myring.devices()

    if gadget_type == "doorbells":
        doorbells = devices['doorbots']
        enumerate_doorbells(doorbells)

        return myring, doorbells

def ts(filename_format: bool = False, dirname_format: bool = False):
    if dirname_format:
        return f"{datetime.now().strftime('%Y-%m-%d')}/"
    if filename_format:
        return datetime.now().strftime('%H-%M-%S')
    else:
        return datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def construct_local_filename(triggered_device, device_name: str, last_event_type: str, newest_event_id: int):
    filename_timestamp = triggered_device.history(limit=1, enforce_limit=True)[0]['created_at'].replace(
        tzinfo=timezone.utc).astimezone(tz=None).strftime('%H-%M-%S')

    local_filename = f"{videopath}{daily_directory()}{device_name.replace(' ', '')}_{last_event_type}_" \
                     f"{filename_timestamp}_{str(newest_event_id)[-4:]}.mp4"

    return local_filename


def daily_directory():
    daily_directory_path = videopath + ts(dirname_format=True)

    if os.path.isdir(daily_directory_path):
        return ts(dirname_format=True)
    else:
        os.mkdir(daily_directory_path)
        print(f"{ts()}: Created new daily directory at {daily_directory_path}")
        return ts(dirname_format=True)


def token_updated(token):
    cache_file.write_text(json.dumps(token))


def otp_callback():
    auth_code = input("2FA code: ")
    return auth_code


def enumerate_doorbells(doorbells):
    for doorbell in doorbells:
        print(f"{ts()}: Authentication and discovery successful for {doorbell}.")


def notify_domoticz(device_name: str, this_event_type: str):
    try:
        if this_event_type == "ding":
          idx = domoticz_idx_mapping_ding.get(device_name)
        if this_event_type == "motion":
          idx = domoticz_idx_mapping_motion.get(device_name)
        urllib.urlopen(f'{domoticz_api}/json.htm?type=command&param=switchlight&idx={idx}&switchcmd=On')
        print(f"{ts()}: Domoticz (idx:{idx}) notified of event at {device_name}")
    except:
        print(f"{ts()}: Error switching to domoticz : '{domoticz_api}/json.htm?type=command&param=switchlight&idx={idx}&switchcmd=On'.")


def get_device_by_id(this_device_id: str, doorbells):
    try:
        this_device = next((doorbell for doorbell in doorbells if doorbell.id == this_device_id), None)
        return this_device
    except:
        return 0


def main():
    myring, doorbells = authenticate_and_initialize(gadget_type="doorbells")

    if myring and doorbells:
        print(f"{ts()}: Entering observation loop - polling interval set to {poll_interval}s\n")

    last_event_id = 0

    while True:

        myring.update_dings()
        alerts = myring.active_alerts()
        if alerts:
            for alert in alerts:
                try:
                    if alert['kind'] == 'ding':
                    	this_event_id = alert['id']
#                   	 print(f"{ts()}: EVENT (last-id: {last_event_id}) -  (this-id: {this_event_id})")
                    	if this_event_id != last_event_id:
                        	last_event_id = this_event_id
                        	this_device = get_device_by_id(alert['doorbot_id'], doorbells)
	                        this_device_name = this_device.name #alert['doorbot_description'] werkt niet meer
                        	this_event_type = alert['kind']
                        	print(f"{ts()}: {this_device_name} '{this_event_type}' event was triggered (id: {this_event_id})")
                        	notify_domoticz(this_device_name, this_event_type)
                except:
                    time.sleep(poll_interval)
                    continue

            time.sleep(poll_interval)


if __name__ == "__main__":
    main()
    
I've split alert kind to different python scripts to run as independant services... At some point you will experience timeout... Implementing pythons scripts as service with restart option will keep everything running ....

Re: Ring 2 Doorbell

Posted: Friday 17 May 2024 16:53
by bldewit
I keep getting the error

Python[4648]: /User/bin/Python: can't find '__main__' module in '/home/pi/domoticz/scripts/python'

What could be the solution?

Re: Ring 2 Doorbell

Posted: Monday 17 February 2025 22:53
by Gingerpale
I like the script by Korrel very much and it works nicely.
Also for motion if you add/change the following:

Code: Select all

for alert in alerts:
                try:
                    if alert['kind'] == 'ding':
                    	this_event_id = alert['id']
#                   	 print(f"{ts()}: EVENT (last-id: {last_event_id}) -  (this-id: {this_event_id})")
                    	if this_event_id != last_event_id:
                        	last_event_id = this_event_id
                        	this_device = get_device_by_id(alert['doorbot_id'], doorbells)
	                        this_device_name = this_device.name #alert['doorbot_description'] werkt niet meer
                        	this_event_type = alert['kind']
                        	print(f"{ts()}: {this_device_name} '{this_event_type}' event was triggered (id: {this_event_id})")
                        	notify_domoticz(this_device_name, this_event_type)
                    if alert['kind'] == 'motion':
                    	this_event_id = alert['id']
#                   	 print(f"{ts()}: EVENT (last-id: {last_event_id}) -  (this-id: {this_event_id})")
                    	if this_event_id != last_event_id:
                        	last_event_id = this_event_id
                        	this_device = get_device_by_id(alert['doorbot_id'], doorbells)
	                        this_device_name = this_device.name #alert['doorbot_description'] werkt niet meer
                        	this_event_type = alert['kind']
                        	print(f"{ts()}: {this_device_name} '{this_event_type}' event was triggered (id: {this_event_id})")
                        	notify_domoticz(this_device_name, this_event_type)

                except:
                    time.sleep(poll_interval)
                    continue

            time.sleep(poll_interval)


if __name__ == "__main__":
    main()
However, I don't get any captures/recordings (yet). Is this because I need (at least) a Basic monthly subscription to capture events?