Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# v2.18.0

This release continues the move of configuration from YAML into the configuration web page - now for *Recipients* and *Transports*. This makes it easy to add an email address for people so they automatically get mailed, pick which mobile devices get included, tune deliveries and include people who aren't Home Assistant users.
This release continues the move of configuration from YAML into the configuration web page - now for *Recipients* and *Transports*. This makes it easy to add an email address for people so they automatically get mailed, pick which mobile devices get included, tune deliveries and include people who aren't Home Assistant users. It also adds WebOS TV notifications.

## 🚨 Breaking changes
- *Recipients*: `recipients:` in YAML is imported into the integration at start up, and ignored in the YAML from then on, with a repair asking for it to be removed
Expand All @@ -19,6 +19,11 @@ This release continues the move of configuration from YAML into the configuratio
- *Housekeeping*: a **Deprecated Entities** option removes the deprecated recipient, delivery and transport `binary_sensor` entities an install carried over from an earlier version, along with their repairs - only offered while there are some
- *Transports*: a repair names any deprecated settings a transport still has, such as `device_domain` directly on it rather than in its options, and saving the transport in the integration moves them to their replacements
- *Conditions*: `notification_actions` and `notification_action_groups` condition variables, holding the `actions` and `action_groups` given on the notify action, so a scenario can send notifications with buttons only to deliveries that can show them - see the [Send Buttons Only Where They Work](https://supernotify.rhizomatics.org.uk/latest/recipes/buttons_where_supported/) recipe ([#311](https://github.com/rhizomatics/supernotify/issues/311))
- *Transports*: [LG webOS TV](https://supernotify.rhizomatics.org.uk/latest/transports/webostv/) transport, set up automatically with the LG webOS TV integration, showing a toast on each TV targeted directly or by area, floor, label or device, and skipping TVs that are off ([#313](https://github.com/rhizomatics/supernotify/issues/313))
- *Deliveries*: an `area_id`, `floor_id` or `label_id` target now selects a delivery by what's in it, so an area with an Alexa echo uses `alexa_devices`, rather than the generic notify entity delivery

## 🐛 Bug fixes
- *Archive*: a delivery its transport skipped now keeps the transport's reason, such as `NO_TARGET` for Kodi or Discord with no valid target, rather than always showing `UNKNOWN`

## 📝 Other changes
### Development
Expand Down
2 changes: 2 additions & 0 deletions custom_components/supernotify/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@
TRANSPORT_MATRIX = "matrix"
TRANSPORT_KODI = "kodi"
TRANSPORT_DISCORD = "discord"
TRANSPORT_WEBOSTV = "webostv"
TRANSPORT_VALUES = [
TRANSPORT_SMS,
TRANSPORT_MQTT,
Expand All @@ -321,6 +322,7 @@
TRANSPORT_MATRIX,
TRANSPORT_KODI,
TRANSPORT_DISCORD,
TRANSPORT_WEBOSTV,
]

# The master list of target category names, independent of any one transport - both
Expand Down
9 changes: 9 additions & 0 deletions custom_components/supernotify/hass_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ def config_path(self, path: str) -> str:
"""A path relative to the Home Assistant config directory, or the path itself if absolute."""
return self._hass.config.path(path)

def config_entry_title_for_entity(self, entity_id: str) -> str | None:
"""The title of the config entry that registered this entity, if any."""
entity_registry: EntityRegistry | None = self._entity_registry()
reg_entry: RegistryEntry | None = entity_registry.async_get(entity_id) if entity_registry else None
if reg_entry is None or reg_entry.config_entry_id is None:
return None
config_entry = self._hass.config_entries.async_get_entry(reg_entry.config_entry_id)
return config_entry.title if config_entry else None

def device_for_entity(self, entity_id: str) -> str | None:
"""The device this entity belongs to, if any."""
entity_registry: EntityRegistry | None = self._entity_registry()
Expand Down
1 change: 1 addition & 0 deletions custom_components/supernotify/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"trace",
"tts",
"twilio_sms",
"webostv",
"workday"
],
"categories": [
Expand Down
9 changes: 7 additions & 2 deletions custom_components/supernotify/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,10 +911,12 @@ def _category_satisfied(self, categories: list[str | TargetEntityCategory], pool
later, so this can be generous (a plain-string category needs a non-empty value list; a
`TargetEntityCategory` needs any pool entity_id whose domain/platform matches, group
entities expanded first since a bare `group.*` id's own domain never matches anything -
it's the members' domain that counts)."""
it's the members' domain that counts, and area/floor/label selectors resolved to their
entities, so a TV in the targeted area counts)."""
if not categories:
return False
hass_api: HomeAssistantAPI = self.context.hass_api
pool = pool.resolve_selectors(hass_api)
entity_ids: list[str] = hass_api.expand_group(pool.entity_ids) if pool.entity_ids else []
for category in categories:
if isinstance(category, TargetEntityCategory):
Expand Down Expand Up @@ -1012,7 +1014,10 @@ async def call_transport(
self.record_result(delivery, envelope)
else:
_LOGGER.info("SUPERNOTIFY No delivery for %s (id: %s)", delivery.name, envelope.notification_id)
self.record_result(delivery, envelope, suppression_reason=SuppressionReason.UNKNOWN)
# keep a reason the transport gave, such as NO_TARGET when every target was skipped
self.record_result(
delivery, envelope, suppression_reason=envelope.skip_reason or SuppressionReason.UNKNOWN
)
except Exception as e2:
envelope.error_count = envelope.error_count + 1
transport.record_error(str(e2), method="deliver")
Expand Down
2 changes: 2 additions & 0 deletions custom_components/supernotify/static_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .transports.sms import SMSTransport
from .transports.telegram import TelegramTransport
from .transports.tts import TTSTransport
from .transports.webostv import WebOsTvTransport

TRANSPORTS: list[type[Transport]] = [
EmailTransport,
Expand All @@ -43,6 +44,7 @@
MatrixTransport,
KodiTransport,
DiscordTransport,
WebOsTvTransport,
] # No auto-discovery of transport plugins so manual class registration required here

TRANSPORT_NAMES = [t.name for t in TRANSPORTS]
103 changes: 103 additions & 0 deletions custom_components/supernotify/transports/webostv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""LG webOS TV transport for SuperNotify.

Shows a toast on LG webOS TVs set up with Home Assistant's `webostv` integration. That
integration has a legacy notify action for each TV, named after its config entry, such as
`notify.living_room_tv`, rather than a notify entity. So the TV's `media_player` entity is the
target, and each one is mapped to its notify action through its config entry. As an entity,
it can be reached by area, floor, label or device too.

Only the message is shown: the title is used only when there is no message.

TVs that are off, or unavailable, are skipped, since the integration can't reach them and
would otherwise raise an error.

Supported data keys (optional):
icon str path to an image file on the Home Assistant host, shown as the toast's icon
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from homeassistant.components.notify.const import ATTR_DATA, ATTR_MESSAGE
from homeassistant.const import ATTR_ICON, STATE_OFF, STATE_UNAVAILABLE
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify

from custom_components.supernotify.const import TRANSPORT_WEBOSTV
from custom_components.supernotify.model import DebugTrace, SuppressionReason, TransportFeature
from custom_components.supernotify.target import TargetEntityCategory
from custom_components.supernotify.transport import Transport

if TYPE_CHECKING:
from custom_components.supernotify.envelope import Envelope
from custom_components.supernotify.hass_api import HomeAssistantAPI

_LOGGER = logging.getLogger(__name__)

HA_WEBOSTV_DOMAIN = "webostv"


class WebOsTvTransport(Transport):
"""Notify via a toast on LG webOS TVs, using the webostv integration"""

name = TRANSPORT_WEBOSTV
integration_domain = HA_WEBOSTV_DOMAIN

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

@property
def supported_features(self) -> TransportFeature:
return TransportFeature.MESSAGE

@property
def unique_target_categories(self) -> list[str | TargetEntityCategory]:
return [TargetEntityCategory(domain="media_player", platform=HA_WEBOSTV_DOMAIN)]

def is_viable(self, hass_api: HomeAssistantAPI) -> bool:
return hass_api.find_config_entry_data(HA_WEBOSTV_DOMAIN) is not None

def build_standard_deliveries(self, hass_api: HomeAssistantAPI) -> dict[str, ConfigType]:
return {self.name: {}}

def validate_action(self, action: str | None) -> bool:
"""The action is found for each TV, not configured"""
return action is None

def notify_action(self, entity_id: str) -> str | None:
"""The TV's legacy notify action, named by the integration after its config entry"""
title: str | None = self.hass_api.config_entry_title_for_entity(entity_id)
if not title:
return None
service: str = slugify(title)
return f"notify.{service}" if self.hass_api.has_service("notify", service) else None

async def deliver(self, envelope: Envelope, debug_trace: DebugTrace | None = None) -> bool:
_LOGGER.debug("SUPERNOTIFY webostv %s", envelope.message)

action_data: dict[str, Any] = {ATTR_MESSAGE: envelope.message or ""}
icon: str | None = (envelope.data or {}).get(ATTR_ICON)
if icon:
action_data[ATTR_DATA] = {ATTR_ICON: icon}

sent: int = 0
for entity_id in envelope.target.entity_ids:
state = self.hass_api.get_state(entity_id)
if state is not None and state.state in (STATE_OFF, STATE_UNAVAILABLE):
_LOGGER.debug("SUPERNOTIFY webostv skipping %s, which is %s", entity_id, state.state)
continue
action: str | None = self.notify_action(entity_id)
if action is None:
_LOGGER.warning("SUPERNOTIFY webostv no notify action found for %s", entity_id)
continue
if await self.call_action(
envelope, qualified_action=action, action_data=dict(action_data), implied_target=True, single_target=True
):
sent += 1

if not sent and not envelope.error_count:
envelope.skipped = 1
envelope.skip_reason = SuppressionReason.NO_TARGET
return sent > 0
4 changes: 3 additions & 1 deletion docs/configuration/deliveries.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A Delivery gets configured automatically for every usable [transport](../transpo

For example, if there is an [SMTP Integration](https://www.home-assistant.io/integrations/smtp/) an `email` Delivery will be set up, with the SMTP `action`.

Whether a standard delivery counts as `default` (auto-selected) or `explicit` (only fires when named) depends on whether its transport can definitively claim something in the notification. `email`, `sms`, `mobile_push`, `discord`, `matrix`, `mqtt`, `telegram`, `html5`, `alexa_devices` and the generic notify-entity delivery are all `default`: each has its own dedicated target category (an email address, a phone number, a `discord_channel:`/`matrix_room:`/`topic:`/`telegram_chat_id:`-qualified value, a matching `notify.*` entity), so they only actually fire once a notification's target or an available recipient's own details give them something to claim - a bare `message`, with nothing for any of them to match, reaches none of them. Transports without a dedicated category of their own - `kodi`, `tts`, the generic `media` transport, `alexa_media_player`, `chime`, `persistent`, `generic`, `ntfy`, `gotify`, `lametric`, `pushover` - stay `explicit` always, since there's no way to tell from the target alone that a notification was meant for them (a bare `media_player.x` entity, for instance, doesn't say whether the intent was an image, a sound, speech, or Kodi's own playback).
Whether a standard delivery counts as `default` (auto-selected) or `explicit` (only fires when named) depends on whether its transport can definitively claim something in the notification. `email`, `sms`, `mobile_push`, `discord`, `matrix`, `mqtt`, `telegram`, `html5`, `alexa_devices`, `webostv` and the generic notify-entity delivery are all `default`: each has its own dedicated target category (an email address, a phone number, a `discord_channel:`/`matrix_room:`/`topic:`/`telegram_chat_id:`-qualified value, a matching `notify.*` entity, an LG TV's `media_player` entity), so they only actually fire once a notification's target or an available recipient's own details give them something to claim - a bare `message`, with nothing for any of them to match, reaches none of them. Transports without a dedicated category of their own - `kodi`, `tts`, the generic `media` transport, `alexa_media_player`, `chime`, `persistent`, `generic`, `ntfy`, `gotify`, `lametric`, `pushover` - stay `explicit` always, since there's no way to tell from the target alone that a notification was meant for them (a bare `media_player.x` entity, for instance, doesn't say whether the intent was an image, a sound, speech, or Kodi's own playback).

In any case, the `inclusion` key can be used, either in the Transport or Delivery configuration, to override any of the automatic choices. See [Delivery Inclusion](#delivery-inclusion) for more on how that works.

Expand Down Expand Up @@ -185,6 +185,8 @@ delivery:
The primary purpose of the `default` (or "implicit") deliveries is to provide a means of handling known targets. For example, if an email address is in the list of targets, it implies that an `email` delivery
is used for it, even if `email` is not on the list. If there are no email addresses the `email` delivery does nothing. Likewise for `discord_channel:xxx` or `topic:my_mqtt_queue` type target addresses.

An `area_id`, `floor_id` or `label_id` target counts by what's in it, so targeting the lounge uses the `webostv` delivery if there's an LG TV in the lounge, and `alexa_devices` if there's an Echo.


### Delivery Control

Expand Down
4 changes: 4 additions & 0 deletions docs/developer/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ Rethink `footer`, which is a key in `data` that only plain emails use. For examp
- Support `dir`, for right-to-left languages, perhaps set from the Home Assistant language as `lang` now is
- Support `timestamp`, for example from when a camera event happened rather than when the notification was sent

### LG webOS TV

The LG webOS TV integration still uses a legacy notify action for each TV, named after its config entry, so the `webostv` transport maps each TV's `media_player` entity to that action through its config entry, to reach TVs by area. If the integration moves to notify entities, as Home Assistant is doing for notify platforms, the `notify_entity` transport would cover the TVs, area included. Then the transport could be dropped, or kept only for skipping TVs that are off.

### Alert

Review integration with `alert` - align live activities where possible with its behaviour and syntax, add recipe, make sure it works well with `notifiers`.
Expand Down
70 changes: 70 additions & 0 deletions docs/transports/webostv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
tags:
- transport
- webostv
---
# LG webOS TV Transport Adaptor

## Motivation

Shows notifications as a toast on LG webOS TVs, through the Home Assistant
[LG webOS TV](https://www.home-assistant.io/integrations/webostv/#notifications) integration.

The integration gives each TV a notify action of its own, such as `notify.living_room_tv`, rather than a notify
entity, so on their own the TVs can't be reached by area. This transport targets each TV's `media_player`
entity instead, and finds its notify action, so a TV can be reached by area, floor, label or device as well as by
its entity.

## Features

* No set up: a `webostv` delivery is made whenever the LG webOS TV integration is set up.
* Only sent to a TV when it is targeted, directly or by its area, floor, label or device, so a TV isn't
interrupted by every notification in the house.
* TVs that are off, or unavailable, are skipped, as the integration can't reach them.
* Only the message is shown. The title is used only when there's no message.

## Configuration

Nothing is needed. Target the TV, or where it is:

```yaml
action: notify.supernotify
data:
message: Dinner is ready
target:
area_id: lounge
```

Targeting the whole ground floor reaches every TV on it that's on, as well as anything else there, such as Alexa
devices:

```yaml
action: notify.supernotify
data:
message: Someone's at the door
target:
floor_id: ground
```

## webOS TV Data Keys

* `icon` — path to an image file on the Home Assistant host, shown as the toast's icon. Any other `data` is left
out, since the integration takes nothing else

```yaml
action: notify.supernotify
data:
message: Dinner is ready
target:
area_id: lounge
data:
icon: /config/www/dinner.png
```

## Notes

* The notify action is named by the integration after the TV, as it was called when set up. After renaming the TV,
its notify action keeps the old name until the integration is reloaded.
* A TV that is skipped because it's off isn't an error, so a delivery `fallback:` isn't used for it. To reach
someone another way when the TV is off, use a [scenario](../configuration/scenarios.md) with a condition on the
TV's state.
2 changes: 1 addition & 1 deletion docs/usage/targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Categories marked *Auto* are recognized from the format of the target itself. Th

| Category | Auto | Used By |
| --------------------------------- | ---- | ------- |
| `entity_id` | Y | Alexa Devices, Alexa Media Player, Chime, HTML5, Kodi, Media Player, Notify Entity, TTS |
| `entity_id` | Y | Alexa Devices, Alexa Media Player, Chime, HTML5, Kodi, LG webOS TV, Media Player, Notify Entity, TTS |
| `device_id` | Y | Chime |
| `email` | Y | Email |
| `mobile_app_id` | Y | Mobile Push, TTS |
Expand Down
Loading
Loading