Ring deurbel domoticz

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Weet iemand als deze bel inmiddels werkt met domoticz.
Hier en daar al wat gezocht maar niks kunnen vinden.
 

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Heeft niemand een antwoord?
Ik wordt bestookt met mailtjes voor een abo op mijn deurbel.:ROFLMAO:
 

Black Tiger

Addicted Member
Administrator
Moderator
Lid sinds
8 feb 2001
Berichten
32.773
Waarderingsscore
306
Punten
83
Leeftijd
58
Locatie
State Penitentiary
Hahaha een abo op je deurbel.
Zelf heb ik nog geen domoticz of domotica maar ik ben eens voor je gaan kijken en jawel hoor. Er schijnt toch het nodige over te doen te zijn op het domoticz forum. Zoals deze bijvoorbeeld (is wel Engels):
 

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Ook is er veel te vinden in het nederlands BT
Ik had een beetje gehoopt hier wat delen.
 

Black Tiger

Addicted Member
Administrator
Moderator
Lid sinds
8 feb 2001
Berichten
32.773
Waarderingsscore
306
Punten
83
Leeftijd
58
Locatie
State Penitentiary
Misschien nog iemand anders. Zoals gezegd heb ik helaas nog geen Domotica, wil er wel nog mee beginnen, misschien dit jaar nog ergens.
De ring deurbel echter niet, want het nadeel van die bellen vind ik dat niet jij alleen maar de halve straat 's avonds hoort dat er iemand voor je deur staat. :)
 

Dr.Z

Addicted Member
Special Friend
Lid sinds
10 feb 2001
Berichten
2.180
Waarderingsscore
83
Punten
48
Leeftijd
52
Locatie
Ergens daarzo..
Is dat niet zo'n google ding?

Ondertussen heb ik in Domoticz gekeken of Ring ondersteund wordt, maar ik heb nog een oude versie dus ik kon hem er niet tussen vinden.
Maar Syno heeft 'surveillance station' voor camerasystemen en volgens mij kun je die gebruiken voor Ring.
 

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Ja je kan idd wel iets.
Maar ring wil pegels.
Ik denk dat ze Josef hebben gevraagd voor het dicht timmeren.
 

Black Tiger

Addicted Member
Administrator
Moderator
Lid sinds
8 feb 2001
Berichten
32.773
Waarderingsscore
306
Punten
83
Leeftijd
58
Locatie
State Penitentiary
Zou het helpen als ik een stukje van dat domoticz forum voor je zou vertalen v.w.b. aansturing van die Ring deurbel?
 

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Is prima
Want ik denk dat wat jij hebt gevonden ik nog niet heb gezien.
Ik zoek idd altijd uitleg in het nederlands.
 

Black Tiger

Addicted Member
Administrator
Moderator
Lid sinds
8 feb 2001
Berichten
32.773
Waarderingsscore
306
Punten
83
Leeftijd
58
Locatie
State Penitentiary
Wil ik wel graag doen alleen kan ik niet alles vertalen. Wat ik wel zie is dat pip3 het eerst geinstalleerd moet zijn.

Dit schijnt nu te werken, zal zoveel mogelijk proberen te vertalen, maar dat doe ik straks of morgen even.
Origineel is hier (klik).

First some prerequisites:
1) You must make sure you pip3 install the ring_doorbell package (https://github.com/tchellomello/python-ring-doorbell) and make sure you are running this in a python3.6+ runtime not python2.x
2) Keep in mind, my home automation setup is micro service based with each workload running on a microk8s home kubernetes cluster consisting of 4 Rasp Pi's. The code I am sharing runs as a service and is NOT some kind of Domoticz drop-in plugin. It's just raw python code which triggers a Domoticz virtual switch and actions can be linked to state changes on that switch from inside Domoticz. How you implement this code snippet is up to you. The intended audience is not the average user, I guess.
3) The code below is tested and seems to be stable for 2 doorbell devices (a front and back doorbell). Should work for more by adding additional 'if device_name == "3rd device name"' statements.
5) The device this code is developed for and tested on is a Ring Video Doorbell Wired (the cheapest, smallest one with no battery - 2020 version). I dont see why it should not work with the other devices as well.
6) On first run, it will ask you for a 2FA code which for me is sent by text message to my configured tel#
6.5) Pay CLOSE attention to the first lines of output on startup. This will tell you the names of your configured devices and you need to use this to configure the name to IDX mapping in the top config section. They must be named the same or Domoticz will not be notified.
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.
8) I have no idea if this will work if you do not have at least a basic subscription. Would be nice to know if anyone wants to test and report back?
9) You must read, understand, and edit the top section of this code to match your own environment
10) This is untested under windows and therefore not guaranteed to be working at all. More robust cross platform refactoring would need to be implemented for windows file paths and maybe more. Not my passion to worry about windows OS's
:D

Features / Notes:
- will watch for any new alerts triggered from your doorbells and immediately toggle a Domoticx IDX# to On(you need to set the IDX to match your own setup)
- allows some time for the video to finalize and upload, and then will download the video to your chosen location - in my case, my local NAS (look at the top section with the comment '# Edit these' and edit accordingly).
- The IDX's in the code below I set up with an Off Delay set to 60 seconds so that it will "clear the motion" on the alarm after 60 seconds and not always remain in the alarm state. This will need configured manually by you in the Domoticz web UI.
- Each event spawns a new thread to wait for the video to finalize and then will download the video from the event without blocking the main polling thread
Ok here is the code (I am updating this as i work on it):
Code:
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 = 'CHANGEME'
your_password = 'CHANGEME'
your_unique_api_name = 'PythonMonitorAPI/0.1.1'
cache_file = Path("/app/Ring/cached_auth_token.json")
videopath = '/app/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.240:30000'
domoticz_idx_mapping = {"Front Door": 528, "Back Door": 529}
#################################################################################################

###### 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):
    idx = domoticz_idx_mapping.get(device_name)
    urllib.urlopen(f'{domoticz_api}/json.htm?type=command&param=switchlight&idx={idx}&switchcmd=On')
    print(f"{ts()}: Domoticz notified of event at {device_name}")


def find_newest_recording_id(triggered_device):
    last_recorded_id = triggered_device.last_recording_id or None

    if len(triggered_device.history(limit=1, enforce_limit=True)) > 0:
        newest_history_id = triggered_device.history(limit=1, enforce_limit=True)[0].get('id')

        if last_recorded_id and newest_history_id:
            newest_item = max(last_recorded_id, newest_history_id)
            return newest_item
        else:
            return None


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 "on_demand" in last_event_type:
            print(f"{ts()}: skipping {device_name} '{last_event_type}' event (id: {newest_event_id})")
            return None, None, None
        else:
            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


def download_recording(newest_event_id: int = None, local_filename: str = None, device_name: str = None,
                       doorbells=None):
    triggered_device = next((doorbell for doorbell in doorbells if doorbell.name == device_name), None)
    print(f"{ts()}: Attempting to download recording for {device_name} (id: {newest_event_id})...\n")

    if newest_event_id and local_filename and triggered_device:
        remaining_download_tries = 5

        while remaining_download_tries > 0:
            try:
                if triggered_device.recording_download(newest_event_id, filename=local_filename, override=True,
                                                       timeout=120):
                    print(f"{ts()}: Saved {device_name} video to: {local_filename}")
            except:
                print(f"{ts()}: Error downloading {device_name} video from on try number "
                      f"{str(6 - remaining_download_tries)}.")
                remaining_download_tries = remaining_download_tries - 1
                time.sleep(5.0)
                continue
            else:
                print(f"{ts()}: {device_name} (id: {newest_event_id}) download thread complete.\n")
                break


def queue_download(newest_event_id: int = None, local_filename: str = None, device_name: str = None, doorbells=None):
    if newest_event_id and local_filename and device_name:
        thread = Thread(target=download_recording,
                        args=(newest_event_id, local_filename, device_name, doorbells))
        thread.start()
        print(
            f"{ts()}: Download thread created (id: {newest_event_id}). Recording of this event is queued for download.")
    else:
        print(f"{ts()}: Unexpected error in queue_download()\n")


def queue_download_to_local_file(newest_event_id, triggered_device, last_event_type: str, device_name: str, doorbells):
    local_filename = construct_local_filename(triggered_device, device_name, last_event_type, newest_event_id)

    print(f"{ts()}: Spawning download thread for {device_name} (id: {newest_event_id})...")
    queue_download(newest_event_id, local_filename, device_name, doorbells)


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:
                    device_name = alert['doorbot_description']
                    triggered_device, last_event_type, newest_event_id = check_device_has_active_alerts(device_name,
                                                                                                        doorbells)

                    if triggered_device and last_event_type and newest_event_id:
                        queue_download_to_local_file(newest_event_id, triggered_device, last_event_type, device_name,
                                                     doorbells)
                except:
                    time.sleep(poll_interval)
                    continue

        time.sleep(poll_interval)


if __name__ == "__main__":
    main()

Hope this is useful to someone! Enjoy!
Cheers!
DR

Edit 1: Added some more detail, updated version of code, and an idea of what it looks like when its running.
Edit 2: Version 0.1.1 - fixed .env file parsing if one exists.
Edit 3: Version 0.1.2 - added multithreading, added graceful handling of common errors/timeouts and implemented some basic retry logic on encountering errors, added grouping of videos into daily directories/folders, event type detection and labeling in logging and file names
Edit 4: Version 0.2.0 - refactored main() loop to separate areas of concern, moved all hardcoded configuration into the top configuration section, updated documentation, removed excessive sleep (the API only seems to work in such a way that each new event retrieves only the video from the event before it - so you are always one event behind).
Edit 5: Version 0.2.2 - switched from retrieving videos from a download URL to using the recording_download() method which also puts a timestamp in the video which i find nifty. Still have not cracked the "always behind by one event" issue when downloading but I personally dont find that too annoying. Added a filter to ignore live view sessions (on_demand event types)
 

janblues

Addicted Member
Special Friend
Lid sinds
15 okt 2006
Berichten
1.879
Waarderingsscore
101
Punten
63
Leeftijd
69
Locatie
Baflo
Bedankt BT
Ik ga er mee aan de slag.
Vertalen hoeft niet hoor.
Het vinden is vaak meer mijn probleem.
 



Hosting Fun

Bovenaan Onderaan