Skip to content
Closed
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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ homeassistant.components.telegram_bot.*
homeassistant.components.teslemetry.*
homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
homeassistant.components.threema.*
homeassistant.components.threshold.*
homeassistant.components.tibber.*
homeassistant.components.tile.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

208 changes: 208 additions & 0 deletions homeassistant/components/threema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Threema Gateway Integration for Home Assistant

Send secure, end-to-end encrypted messages from Home Assistant to any Threema user using the Threema Gateway service.

## What is Threema Gateway?

[Threema Gateway](https://gateway.threema.ch/) allows you to send Threema messages programmatically. This integration enables Home Assistant to send notifications and alerts to Threema users.

**Common Use Cases:**
- Security alerts (door/window sensors)
- Temperature warnings
- System notifications
- Daily summaries

## Prerequisites

1. **Threema Gateway Account** - Sign up at [gateway.threema.ch](https://gateway.threema.ch/)
2. **Gateway Credentials** - You'll receive a Gateway ID (starts with `*`) and API Secret
3. **Message Credits** - Purchase credits to send messages

## Installation

### Step 1: Add Integration

1. Go to **Settings** > **Devices & Services**
2. Click **+ Add Integration**
3. Search for **"Threema"**
4. Follow the setup wizard

### Step 2: Choose Setup Type

#### Option A: Create NEW Gateway ID (Recommended for E2E)
1. Select "Create NEW Gateway ID"
2. Home Assistant generates a key pair
3. **Copy the public key** shown on screen (hex part only, without `public:` prefix)
4. Go to https://gateway.threema.ch/ and create an **End-to-End Gateway ID**
5. **Paste the public key** during registration
6. Return to Home Assistant, enter your new Gateway ID and API Secret
7. Keys are automatically stored

#### Option B: Use EXISTING Gateway ID
1. Select "Use EXISTING Gateway ID"
2. Enter your Gateway ID and API Secret
3. **For E2E mode:** Also enter your private key (format: `private:abc123def456...`)
4. **Optionally:** Enter your public key (format: `public:def456abc123...`)
5. **For Basic mode:** Leave key fields empty

**Key Format Examples:**
```
Private Key: private:1a2b3c4d5e6f7890abcdef1234567890...
Public Key: public:9876543210fedcba0987654321fedcba...
```

Comment on lines +1 to +53
## Usage

### Service: `threema.send_message`

This is the primary way to send messages. Use it in automations, scripts, or Developer Tools > Services.

```yaml
service: threema.send_message
data:
recipient: "ABCD1234" # Threema ID of the recipient (8 characters)
message: "Hello from Home Assistant!"
```

The integration automatically uses your configured gateway. If you have multiple gateways, specify which one:
```yaml
service: threema.send_message
data:
config_entry_id: "01kh8feq7yz5qrj4d974sdg1w3"
recipient: "ABCD1234"
message: "Hello from Home Assistant!"
```

**How to find a Threema ID:**
- Open Threema app > Settings > My ID (8 characters, e.g., `ABCD1234`)

### Example Automation

Send an alert when a door opens:

```yaml
automation:
- alias: "Front Door Alert"
trigger:
- platform: state
entity_id: binary_sensor.front_door
from: "off"
to: "on"
action:
- service: threema.send_message
data:
recipient: !secret my_threema_id
message: >
Front door opened at {{ now().strftime('%H:%M:%S') }}
```

**secrets.yaml:**
```yaml
my_threema_id: "YOURID12"
```

## Gateway Verification (QR Code)

When using E2E encryption (public key configured), a **QR code image entity** is created for your gateway device. This allows identity verification:

1. Go to **Settings** > **Devices & Services** > **Threema**
2. Click on your Gateway device
3. Find the **"Gateway QR Code"** image entity
4. Scan the QR code with the Threema app to verify the gateway's public key

The QR code encodes `3mid:<gateway_id>,<public_key_hex>` following the Threema verification format. This is only available when a public key is configured (E2E mode).

## Encryption Modes

### End-to-End (E2E) Mode
- Messages encrypted from Home Assistant to the recipient's device
- Maximum privacy - even Threema servers can't read them
- Requires private key setup

### Basic/Simple Mode
- Messages encrypted between Home Assistant and Threema servers
- Simpler setup, no key management needed
- Still secure for most use cases

## Key Management

**After key generation:**
- Keys are displayed once during setup - save them immediately
- Both keys are stored in Home Assistant's config entries
- Stored in: `/config/.storage/core.config_entries`

**Key Format:**
- Private keys start with `private:` followed by 64 hex characters
- Public keys start with `public:` followed by 64 hex characters

**Backup:** Save your keys securely (password manager or encrypted backup). If you lose your private key, you'll need to create a new Gateway ID.

## Reauthentication

If your credentials become invalid, Home Assistant will prompt you to re-enter your API Secret (and optionally your private key) through the reauthentication flow.

## Troubleshooting

### "Cannot Connect" Error
- Verify Gateway ID starts with `*` and is exactly 8 characters
- Check API Secret is correct
- Ensure you have remaining message credits
- Test credentials at gateway.threema.ch

### Messages Not Delivered
- Check you have credits: gateway.threema.ch > Account > Credits
- Verify recipient Threema ID is correct (8 characters)
- Check logs: Settings > System > Logs (search "threema")

### Enable Debug Logging

Add to `configuration.yaml`:
```yaml
logger:
default: info
logs:
homeassistant.components.threema: debug
```

## Security Best Practices

1. Store recipient IDs in `secrets.yaml`, not directly in automations
2. Backup your private key securely (if using E2E mode)
3. Monitor credit usage to detect abuse
4. Use E2E mode when possible for maximum privacy

## FAQ

**Q: Can I receive messages in Home Assistant?**
A: Not currently. This integration is send-only.

**Q: How much does it cost?**
A: A small amount per message. Check gateway.threema.ch for current pricing.

**Q: What's the message length limit?**
A: 3,500 characters.

**Q: Can I send images or files?**
A: Not yet. Text messages only in the current version.

## Roadmap

### Planned Features
- **Incoming messages via Gateway callbacks** — receive messages sent to your Gateway ID as HA events/triggers for automations (requires a publicly reachable URL, e.g. via Nabu Casa or Cloudflare tunnel)
- Image and file support
- Remaining credits sensor
- Rich media (video, audio, location sharing)
- Group messaging
- Message templates

## Support

- **Threema Gateway Docs:** https://gateway.threema.ch/en
- **HA Community:** https://community.home-assistant.io/
- **Issues:** Report via GitHub

## License

This integration uses the official Threema Gateway Python SDK.
- Home Assistant: Apache License 2.0
- Threema Gateway SDK: MIT License
158 changes: 158 additions & 0 deletions homeassistant/components/threema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""The Threema Gateway integration."""

from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .client import (
ThreemaAPIClient,
ThreemaAuthError,
ThreemaConnectionError,
ThreemaSendError,
)
from .const import CONF_API_SECRET, CONF_GATEWAY_ID, CONF_PRIVATE_KEY, DOMAIN

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS: list[Platform] = [Platform.IMAGE]

type ThreemaConfigEntry = ConfigEntry[ThreemaAPIClient]

CONF_CONFIG_ENTRY_ID = "config_entry_id"
CONF_RECIPIENT = "recipient"
CONF_MESSAGE = "message"

RECIPIENT_SCHEMA = vol.All(
cv.string,
cv.matches_regex(r"^[0-9A-Za-z]{8}$"),
lambda value: value.upper(),
)

SERVICE_SEND_MESSAGE = "send_message"
SERVICE_SEND_MESSAGE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(CONF_RECIPIENT): RECIPIENT_SCHEMA,
vol.Required(CONF_MESSAGE): cv.string,
}
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Threema Gateway component."""

async def async_send_message(call: ServiceCall) -> None:
"""Handle the send_message service call."""
recipient = call.data[CONF_RECIPIENT]
message = call.data[CONF_MESSAGE]

# Get the config entry - auto-select if not specified
entry_id = call.data.get(CONF_CONFIG_ENTRY_ID)

if entry_id:
entry = hass.config_entries.async_get_entry(entry_id)
if not entry or entry.domain != DOMAIN:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_found",
)
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="entry_not_loaded",
)
else:
# Auto-select: find any loaded Threema config entry
entries = [
e
for e in hass.config_entries.async_entries(DOMAIN)
if e.state is ConfigEntryState.LOADED
]
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_entries_found",
)
if len(entries) > 1:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="multiple_entries_found",
)
entry = entries[0]

# Send the message
client: ThreemaAPIClient = entry.runtime_data
try:
await client.send_text_message(recipient, message)
except ThreemaAuthError as err:
_LOGGER.warning(
"Authentication failed sending message; check your Gateway credentials"
)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_error",
translation_placeholders={"error": str(err)},
) from err
Comment on lines +102 to +110
except (ThreemaSendError, ThreemaConnectionError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_error",
translation_placeholders={"error": str(err)},
) from err

hass.services.async_register(
DOMAIN,
SERVICE_SEND_MESSAGE,
async_send_message,
schema=SERVICE_SEND_MESSAGE_SCHEMA,
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ThreemaConfigEntry) -> bool:
"""Set up Threema Gateway from a config entry."""
client = ThreemaAPIClient(
hass,
gateway_id=entry.data[CONF_GATEWAY_ID],
api_secret=entry.data[CONF_API_SECRET],
private_key=entry.data.get(CONF_PRIVATE_KEY),
)

try:
await client.validate_credentials()
except ThreemaAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except ThreemaConnectionError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err

entry.runtime_data = client

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ThreemaConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Loading
Loading