core/homeassistant/components/pushbullet/notify.py

180 lines
6.5 KiB
Python

"""Pushbullet platform for notify component."""
from __future__ import annotations
import logging
import mimetypes
from typing import Any
from pushbullet import PushBullet, PushError
from pushbullet.channel import Channel
from pushbullet.device import Device
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA,
ATTR_TARGET,
ATTR_TITLE,
ATTR_TITLE_DEFAULT,
PLATFORM_SCHEMA,
BaseNotificationService,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import ATTR_FILE, ATTR_FILE_URL, ATTR_URL, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string})
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> PushBulletNotificationService | None:
"""Get the Pushbullet notification service."""
if discovery_info is None:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2023.2.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
return None
pushbullet: PushBullet = hass.data[DOMAIN][discovery_info["entry_id"]].pushbullet
return PushBulletNotificationService(hass, pushbullet)
class PushBulletNotificationService(BaseNotificationService):
"""Implement the notification service for Pushbullet."""
def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None:
"""Initialize the service."""
self.hass = hass
self.pushbullet = pushbullet
@property
def pbtargets(self) -> dict[str, dict[str, Device | Channel]]:
"""Return device and channel detected targets."""
return {
"device": {tgt.nickname.lower(): tgt for tgt in self.pushbullet.devices},
"channel": {
tgt.channel_tag.lower(): tgt for tgt in self.pushbullet.channels
},
}
def send_message(self, message: str, **kwargs: Any) -> None:
"""Send a message to a specified target.
If no target specified, a 'normal' push will be sent to all devices
linked to the Pushbullet account.
Email is special, these are assumed to always exist. We use a special
call which doesn't require a push object.
"""
targets: list[str] = kwargs.get(ATTR_TARGET, [])
title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data: dict[str, Any] = kwargs[ATTR_DATA] or {}
if not targets:
# Backward compatibility, notify all devices in own account.
self._push_data(message, title, data, self.pushbullet)
_LOGGER.debug("Sent notification to self")
return
# refresh device and channel targets
self.pushbullet.refresh()
# Main loop, process all targets specified.
for target in targets:
try:
ttype, tname = target.split("/", 1)
except ValueError as err:
raise ValueError(f"Invalid target syntax: '{target}'") from err
# Target is email, send directly, don't use a target object.
# This also seems to work to send to all devices in own account.
if ttype == "email":
self._push_data(message, title, data, self.pushbullet, email=tname)
_LOGGER.info("Sent notification to email %s", tname)
continue
# Target is sms, send directly, don't use a target object.
if ttype == "sms":
self._push_data(
message, title, data, self.pushbullet, phonenumber=tname
)
_LOGGER.info("Sent sms notification to %s", tname)
continue
if ttype not in self.pbtargets:
raise ValueError(f"Invalid target syntax: {target}")
tname = tname.lower()
if tname not in self.pbtargets[ttype]:
raise ValueError(f"Target: {target} doesn't exist")
# Attempt push_note on a dict value. Keys are types & target
# name. Dict pbtargets has all *actual* targets.
self._push_data(message, title, data, self.pbtargets[ttype][tname])
_LOGGER.debug("Sent notification to %s/%s", ttype, tname)
def _push_data(
self,
message: str,
title: str,
data: dict[str, Any],
pusher: PushBullet,
email: str | None = None,
phonenumber: str | None = None,
):
"""Create the message content."""
kwargs = {"body": message, "title": title}
if email:
kwargs["email"] = email
try:
if phonenumber and pusher.devices:
pusher.push_sms(pusher.devices[0], phonenumber, message)
return
if url := data.get(ATTR_URL):
pusher.push_link(url=url, **kwargs)
return
if filepath := data.get(ATTR_FILE):
if not self.hass.config.is_allowed_path(filepath):
raise ValueError("Filepath is not valid or allowed")
with open(filepath, "rb") as fileh:
filedata = self.pushbullet.upload_file(fileh, filepath)
if filedata.get("file_type") == "application/x-empty":
raise ValueError("Cannot send an empty file")
kwargs.update(filedata)
pusher.push_file(**kwargs)
elif (file_url := data.get(ATTR_FILE_URL)) and vol.Url(file_url):
pusher.push_file(
file_name=file_url,
file_url=file_url,
file_type=(mimetypes.guess_type(file_url)[0]),
**kwargs,
)
else:
pusher.push_note(**kwargs)
except PushError as err:
raise HomeAssistantError(f"Notify failed: {err}") from err