-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
New: Threema Integration #165787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
New: Threema Integration #165787
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
|
|
||
| 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", | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| if entry.state is not ConfigEntryState.LOADED: | ||
| raise ServiceValidationError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="entry_not_loaded", | ||
| ) | ||
| else: | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # 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", | ||
| ) | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if len(entries) > 1: | ||
| raise ServiceValidationError( | ||
| translation_domain=DOMAIN, | ||
| translation_key="multiple_entries_found", | ||
| ) | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| entry = entries[0] | ||
|
|
||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # 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)}, | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) from err | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
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 | ||
LukasQ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.