Module ambianic.notification

Utilities to send notifications.

Expand source code
"""Utilities to send notifications."""

import hashlib
import logging
import os
import urllib
from string import Template

import ambianic
import apprise
from ambianic.configuration import get_root_config
from ambianic.util import jsonify

log = logging.getLogger(__name__)

UI_BASE_URL_DEFAULT = "https://ui.ambianic.ai"


class Notification:
    def __init__(self, envelope: dict = {}, providers: list = ["all"]):
        self.providers: list = providers
        self.title: str = None
        self.message: str = None
        self.attach: list = []
        self.envelope: dict = envelope

    def add_attachments(self, *args):
        self.attach.append(*args)

    def to_dict(self) -> dict:
        return dict(vars(self))


class NotificationHandler:
    def __init__(self, config: dict = None):
        if config is None:
            config = ambianic.configuration.get_root_config()
        self.apobj = apprise.Apprise(debug=True)
        self.config = config.get("notifications", {})
        for name, cfg in self.config.items():
            providers = cfg.get("providers", [])
            for provider in providers:
                if not self.apobj.add(provider, tag=name):
                    log.warning(
                        f"Failed to add notification provider: {name}={provider}"
                    )
                else:
                    log.info(f"Apprise: added notification provider: {name}={provider}")

    def send(self, notification: Notification):
        log.debug("preparing notification")
        for provider in notification.providers:
            log.debug(f"preparing notification payload for provider {provider}")
            cfg = self.config.get(provider, None)

            if cfg:
                enabled = cfg.get("enabled", True)
                if not enabled:
                    log.debug(f"notification disabled for provider {provider}")
                    return
                else:
                    log.debug(f"notification enabled for provider {provider}")

                templates = cfg.get("templates", {})

                title = notification.title
                if title is None:
                    title = templates.get(
                        "title",
                        "[Ambianic.ai] New event - ${event_type}: ${event_labels}",
                    )

                log.debug(f"template title: {title}")

                message = notification.message
                if message is None:
                    message = templates.get(
                        "message", "New event\n ${event_type}: ${event_labels}"
                    )

                log.debug(f"template message: {message}")

                attachments = []
                for a in notification.attach:
                    if not os.path.exists(a) or not os.path.isfile(a):
                        log.warning("Attachment is not a valid file %s")
                        continue
                    attachments.append(a)

                url_params = {**notification.envelope}

                peer_id = get_root_config().get("peerId", None)
                if peer_id is None:
                    log.warning(
                        "peerId not found. Notification will not include link back to peer."
                    )
                else:
                    peerid_hash_input = peer_id + notification.envelope["args"]["id"]
                    peerid_hash = hashlib.sha256(
                        peerid_hash_input.encode("utf-8")
                    ).hexdigest()
                    # send peerid hashed with event_id as salt to avoid replay attacks
                    # UI can lookup the actual peerid be going over its stored peerid's
                    # and generating salted hashes for each
                    # until one matches the URL paramter.
                    url_params["peerid_hash"] = peerid_hash

                # convert args values to JSON format to ensure
                # smooth conversion to JavaScript values on the UI side
                # Fix for bug: https://github.com/ambianic/ambianic-edge/issues/397
                args = url_params["args"]
                url_params["args"] = jsonify(args)
                # URL encode all parameters
                url_query = urllib.parse.urlencode(url_params)

                ui_config = get_root_config().get("ui", {})
                ui_base_url = ui_config.get("baseurl", UI_BASE_URL_DEFAULT)

                event_labels = list(
                    map(
                        lambda i: i["label"],
                        notification.envelope["args"]["inference_result"],
                    )
                )
                event_labels_str = ",".join(event_labels)
                event_source_str = get_root_config().get(
                    "display_name", "My Ambianic Edge Device"
                )

                template_args = {
                    "event_source": event_source_str,
                    "event_type": notification.envelope["args"]["inference_meta"][
                        "display"
                    ],
                    "event_labels": event_source_str + ": " + event_labels_str,
                    "event_details_url": f"{ui_base_url}/event?{url_query}",
                }

                log.debug(f"template_args: {template_args}")

                # resolve template references for title and message
                title = Template(title).safe_substitute(template_args)
                message = Template(message).safe_substitute(template_args)

                log.debug(f"template resolved title: {title}")
                log.debug(f"template resolved message: {message}")

                include_attachments = cfg.get("include_attachments", False)
                ok = self.apobj.notify(
                    title=title,
                    body=message,
                    tag=provider,
                    attach=attachments if include_attachments else [],
                )
                if ok:
                    log.debug(f"Sent notification {template_args} to {provider}")
                else:
                    log.warning(
                        f"Error sending notification {template_args} to {provider}"
                    )
            else:
                log.warning("Skipping unknown provider %s" % provider)
                continue

Classes

class Notification (envelope: dict = {}, providers: list = ['all'])
Expand source code
class Notification:
    def __init__(self, envelope: dict = {}, providers: list = ["all"]):
        self.providers: list = providers
        self.title: str = None
        self.message: str = None
        self.attach: list = []
        self.envelope: dict = envelope

    def add_attachments(self, *args):
        self.attach.append(*args)

    def to_dict(self) -> dict:
        return dict(vars(self))

Methods

def add_attachments(self, *args)
Expand source code
def add_attachments(self, *args):
    self.attach.append(*args)
def to_dict(self) ‑> dict
Expand source code
def to_dict(self) -> dict:
    return dict(vars(self))
class NotificationHandler (config: dict = None)
Expand source code
class NotificationHandler:
    def __init__(self, config: dict = None):
        if config is None:
            config = ambianic.configuration.get_root_config()
        self.apobj = apprise.Apprise(debug=True)
        self.config = config.get("notifications", {})
        for name, cfg in self.config.items():
            providers = cfg.get("providers", [])
            for provider in providers:
                if not self.apobj.add(provider, tag=name):
                    log.warning(
                        f"Failed to add notification provider: {name}={provider}"
                    )
                else:
                    log.info(f"Apprise: added notification provider: {name}={provider}")

    def send(self, notification: Notification):
        log.debug("preparing notification")
        for provider in notification.providers:
            log.debug(f"preparing notification payload for provider {provider}")
            cfg = self.config.get(provider, None)

            if cfg:
                enabled = cfg.get("enabled", True)
                if not enabled:
                    log.debug(f"notification disabled for provider {provider}")
                    return
                else:
                    log.debug(f"notification enabled for provider {provider}")

                templates = cfg.get("templates", {})

                title = notification.title
                if title is None:
                    title = templates.get(
                        "title",
                        "[Ambianic.ai] New event - ${event_type}: ${event_labels}",
                    )

                log.debug(f"template title: {title}")

                message = notification.message
                if message is None:
                    message = templates.get(
                        "message", "New event\n ${event_type}: ${event_labels}"
                    )

                log.debug(f"template message: {message}")

                attachments = []
                for a in notification.attach:
                    if not os.path.exists(a) or not os.path.isfile(a):
                        log.warning("Attachment is not a valid file %s")
                        continue
                    attachments.append(a)

                url_params = {**notification.envelope}

                peer_id = get_root_config().get("peerId", None)
                if peer_id is None:
                    log.warning(
                        "peerId not found. Notification will not include link back to peer."
                    )
                else:
                    peerid_hash_input = peer_id + notification.envelope["args"]["id"]
                    peerid_hash = hashlib.sha256(
                        peerid_hash_input.encode("utf-8")
                    ).hexdigest()
                    # send peerid hashed with event_id as salt to avoid replay attacks
                    # UI can lookup the actual peerid be going over its stored peerid's
                    # and generating salted hashes for each
                    # until one matches the URL paramter.
                    url_params["peerid_hash"] = peerid_hash

                # convert args values to JSON format to ensure
                # smooth conversion to JavaScript values on the UI side
                # Fix for bug: https://github.com/ambianic/ambianic-edge/issues/397
                args = url_params["args"]
                url_params["args"] = jsonify(args)
                # URL encode all parameters
                url_query = urllib.parse.urlencode(url_params)

                ui_config = get_root_config().get("ui", {})
                ui_base_url = ui_config.get("baseurl", UI_BASE_URL_DEFAULT)

                event_labels = list(
                    map(
                        lambda i: i["label"],
                        notification.envelope["args"]["inference_result"],
                    )
                )
                event_labels_str = ",".join(event_labels)
                event_source_str = get_root_config().get(
                    "display_name", "My Ambianic Edge Device"
                )

                template_args = {
                    "event_source": event_source_str,
                    "event_type": notification.envelope["args"]["inference_meta"][
                        "display"
                    ],
                    "event_labels": event_source_str + ": " + event_labels_str,
                    "event_details_url": f"{ui_base_url}/event?{url_query}",
                }

                log.debug(f"template_args: {template_args}")

                # resolve template references for title and message
                title = Template(title).safe_substitute(template_args)
                message = Template(message).safe_substitute(template_args)

                log.debug(f"template resolved title: {title}")
                log.debug(f"template resolved message: {message}")

                include_attachments = cfg.get("include_attachments", False)
                ok = self.apobj.notify(
                    title=title,
                    body=message,
                    tag=provider,
                    attach=attachments if include_attachments else [],
                )
                if ok:
                    log.debug(f"Sent notification {template_args} to {provider}")
                else:
                    log.warning(
                        f"Error sending notification {template_args} to {provider}"
                    )
            else:
                log.warning("Skipping unknown provider %s" % provider)
                continue

Methods

def send(self, notification: Notification)
Expand source code
def send(self, notification: Notification):
    log.debug("preparing notification")
    for provider in notification.providers:
        log.debug(f"preparing notification payload for provider {provider}")
        cfg = self.config.get(provider, None)

        if cfg:
            enabled = cfg.get("enabled", True)
            if not enabled:
                log.debug(f"notification disabled for provider {provider}")
                return
            else:
                log.debug(f"notification enabled for provider {provider}")

            templates = cfg.get("templates", {})

            title = notification.title
            if title is None:
                title = templates.get(
                    "title",
                    "[Ambianic.ai] New event - ${event_type}: ${event_labels}",
                )

            log.debug(f"template title: {title}")

            message = notification.message
            if message is None:
                message = templates.get(
                    "message", "New event\n ${event_type}: ${event_labels}"
                )

            log.debug(f"template message: {message}")

            attachments = []
            for a in notification.attach:
                if not os.path.exists(a) or not os.path.isfile(a):
                    log.warning("Attachment is not a valid file %s")
                    continue
                attachments.append(a)

            url_params = {**notification.envelope}

            peer_id = get_root_config().get("peerId", None)
            if peer_id is None:
                log.warning(
                    "peerId not found. Notification will not include link back to peer."
                )
            else:
                peerid_hash_input = peer_id + notification.envelope["args"]["id"]
                peerid_hash = hashlib.sha256(
                    peerid_hash_input.encode("utf-8")
                ).hexdigest()
                # send peerid hashed with event_id as salt to avoid replay attacks
                # UI can lookup the actual peerid be going over its stored peerid's
                # and generating salted hashes for each
                # until one matches the URL paramter.
                url_params["peerid_hash"] = peerid_hash

            # convert args values to JSON format to ensure
            # smooth conversion to JavaScript values on the UI side
            # Fix for bug: https://github.com/ambianic/ambianic-edge/issues/397
            args = url_params["args"]
            url_params["args"] = jsonify(args)
            # URL encode all parameters
            url_query = urllib.parse.urlencode(url_params)

            ui_config = get_root_config().get("ui", {})
            ui_base_url = ui_config.get("baseurl", UI_BASE_URL_DEFAULT)

            event_labels = list(
                map(
                    lambda i: i["label"],
                    notification.envelope["args"]["inference_result"],
                )
            )
            event_labels_str = ",".join(event_labels)
            event_source_str = get_root_config().get(
                "display_name", "My Ambianic Edge Device"
            )

            template_args = {
                "event_source": event_source_str,
                "event_type": notification.envelope["args"]["inference_meta"][
                    "display"
                ],
                "event_labels": event_source_str + ": " + event_labels_str,
                "event_details_url": f"{ui_base_url}/event?{url_query}",
            }

            log.debug(f"template_args: {template_args}")

            # resolve template references for title and message
            title = Template(title).safe_substitute(template_args)
            message = Template(message).safe_substitute(template_args)

            log.debug(f"template resolved title: {title}")
            log.debug(f"template resolved message: {message}")

            include_attachments = cfg.get("include_attachments", False)
            ok = self.apobj.notify(
                title=title,
                body=message,
                tag=provider,
                attach=attachments if include_attachments else [],
            )
            if ok:
                log.debug(f"Sent notification {template_args} to {provider}")
            else:
                log.warning(
                    f"Error sending notification {template_args} to {provider}"
                )
        else:
            log.warning("Skipping unknown provider %s" % provider)
            continue