28 Commits
0.1.6 ... main

Author SHA1 Message Date
a2db56e704 custom_components/whatsapp_bridge_integration/__init__.py aktualisiert 2026-06-15 09:57:12 +00:00
6a74cc7f2e custom_components/whatsapp_bridge_integration/manifest.json aktualisiert 2026-06-15 09:56:54 +00:00
2701a6f908 CHANGELOG.md aktualisiert 2026-06-15 09:56:41 +00:00
1d85bd7905 custom_components/whatsapp_bridge_integration/sensor.py aktualisiert
0.2.3
2026-06-15 09:52:35 +00:00
d4ede6669a custom_components/whatsapp_bridge_integration/__init__.py aktualisiert
0.2.3
2026-06-15 09:52:16 +00:00
eb6be8cdf4 custom_components/whatsapp_bridge_integration/manifest.json aktualisiert
0.2.3
2026-06-15 09:51:57 +00:00
2c62edfa44 CHANGELOG.md aktualisiert
0.2.3
2026-06-15 09:51:42 +00:00
faed01cd8f CHANGELOG.md aktualisiert 2026-06-15 09:38:41 +00:00
7175522f4a custom_components/whatsapp_bridge_integration/sensor.py aktualisiert
0.2.2
2026-06-15 09:37:06 +00:00
3b99c037ae custom_components/whatsapp_bridge_integration/__init__.py aktualisiert
0.2.2
2026-06-15 09:36:42 +00:00
4f8a4304ba custom_components/whatsapp_bridge_integration/manifest.json aktualisiert
0.2.2
2026-06-15 09:36:19 +00:00
bcff98b44b CHANGELOG.md aktualisiert
0.2.2
2026-06-15 09:36:00 +00:00
40f74f9b8b CHANGELOG.md aktualisiert 2026-06-15 09:27:36 +00:00
5e1c07cecf CHANGELOG.md aktualisiert
0.2.1
2026-06-15 09:26:57 +00:00
2cfb881cc0 custom_components/whatsapp_bridge_integration/sensor.py aktualisiert
0.2.1
2026-06-15 09:26:10 +00:00
c5d216e458 custom_components/whatsapp_bridge_integration/__init__.py aktualisiert
0.2.1
2026-06-15 09:25:47 +00:00
4f08cbbd6d custom_components/whatsapp_bridge_integration/manifest.json aktualisiert
0.2.1
2026-06-15 09:25:26 +00:00
d09ff3d7cf custom_components/whatsapp_bridge_integration/sensor.py aktualisiert
0.2.0
2026-06-15 09:16:40 +00:00
fa7baddeb6 custom_components/whatsapp_bridge_integration/__init__.py aktualisiert 2026-06-15 09:16:13 +00:00
cff4183c46 custom_components/whatsapp_bridge_integration/manifest.json aktualisiert
0.2.0
2026-06-15 09:09:18 +00:00
719a4b5129 CHANGELOG.md
Add Changelog
2026-06-15 09:08:28 +00:00
c536a75f4c README.md aktualisiert 2026-04-14 16:19:46 +00:00
f346cea41a custom_components/whatsapp_bridge_integration/manifest.json aktualisiert 2026-04-14 14:02:20 +00:00
93b57878f9 custom_components/whatsapp_bridge_integration/services.yaml aktualisiert 2026-04-14 14:01:07 +00:00
fc826d7345 custom_components/whatsapp_bridge_integration/services.py aktualisiert 2026-04-14 14:00:54 +00:00
3002acc934 custom_components/whatsapp_bridge_integration/sensor.py aktualisiert 2026-04-14 14:00:36 +00:00
39df1ed0fa custom_components/whatsapp_bridge_integration/services.py aktualisiert 2026-04-14 13:59:19 +00:00
6b3a10bd33 custom_components/whatsapp_bridge_integration/services.yaml aktualisiert 2026-04-14 13:59:02 +00:00
7 changed files with 332 additions and 73 deletions

28
CHANGELOG.md Normal file
View File

@@ -0,0 +1,28 @@
# CHANGELOG - Whatsapp Bridge Integration
## Release 0.2.4 - 2026-06-15
- **Fix:** Resolved a `TypeError` in `async_get_or_create()` caused by stricter Home Assistant Core requirements demanding a `config_entry_id` for all device registry entries.
- **Architecture Refinement:** The global "WhatsApp Bridge Hub" now utilizes a shared multi-entry architecture. It dynamically links to active configurations under a static identifier, ensuring the hub remains a single, non-duplicated entity that survives individual account deletions.
## Release 0.2.3 - 2026-06-15
- **New (Orphaned Devices Cleaner):** Added an automatic database cleanup utility. On startup, the integration now automatically identifies and removes orphaned or "ghost" devices left behind by previous configuration changes or older versions.
- **Fix:** Decoupled the central "WhatsApp Bridge Hub" device from specific config entry bindings. The Hub is now persistent and indestructible, preventing global entities (Broadcast and Live Status) from disappearing when individual user profiles are deleted or re-added.
## Release 0.2.2 - 2026-06-15
- **Architecture Change:** Introduced a dedicated "WhatsApp Bridge Hub" device. This central device acts as a single point of reference for global components, preventing UI clutter.
- **Fix:** Moved the global live status sensor and the virtual broadcast entity to the new WhatsApp Bridge Hub device. This ensures they remain fully visible and accessible across the Home Assistant UI, automations, and service calls without being duplicated across individual user accounts.
- **Improvement:** Maintained isolated, clean individual devices for each configured account profile.
## Release 0.2.1 - 2026-06-15
- **Fix:** Resolved device assignment duplication where a single global gateway card duplicated across all account tabs.
- **Fix:** Globalized the live status sensor and the broadcast entity to instantiate exactly once for the entire integration rather than incorrectly duplicating per account.
- **Optimization:** Code refactoring within `sensor.py` to prevent entity duplication when utilizing multiple profiles.
## Release 0.2.0 - 2026-06-15
- **New:** Setup of a main device (WhatsApp Bridge Gateway) in the Home Assistant device registry.
- **New:** Integrated global service `whatsapp_bridge.send_broadcast`, which operates independently of individual profile entities and can send messages to any number or group ID.
- **New:** Added status sensor (`sensor.whatsapp_bridge_status`). This queries the add-on's new API and provides the current connection status (`CONNECTED`, `WAITING_FOR_SCAN`, `INITIALIZING`, `OFFLINE`) directly within Home Assistant.
- **Improved:** The sensor is assigned to the new main device, which significantly simplifies automations (e.g., alert on connection loss).
## Release 0.1.7
- Previous stable version

128
README.md
View File

@@ -1,58 +1,58 @@
# WhatsApp Bridge Integration for Home Assistant 🚀 # WhatsApp Bridge for Home Assistant
This is the custom integration for the **Bahmcloud WhatsApp Bridge**. It allows you to manage multiple WhatsApp accounts as Home Assistant services, making it incredibly easy to send automated messages via YAML or Node-RED. A professional, seamless integration to send WhatsApp messages through the **Bahmcloud Bridge**. This integration supports multiple profiles, dynamic entity-based selection, and broadcast capabilities with native Home Assistant support.
--- ## 🚀 Installation
## 🛠️ Installation ### 1. Install the Add-on
The **Bahmcloud WhatsApp Bridge Add-on** must be running for the integration to work:
### 1. Prerequisite: The Add-on 1. Navigate to **Settings** -> **Add-ons**.
Before installing this integration, ensure that the **WhatsApp Bridge Add-on** is installed and running. 2. Click **Add-on Store** -> Three dots (top right) -> **Repositories**.
* [Link to your Add-on Repository] 3. Add your Bahmcloud Repository URL.
* Open the Add-on Web UI and scan the QR code to link your WhatsApp account. 4. Search for "WhatsApp Bridge", click **Install**, and **Start**.
### 2. Install the Integration ### 2. Install the Integration
1. Copy the folder `whatsapp_bridge_integration` into your Home Assistant `config/custom_components/` directory. You can install the integration component via three methods:
2. Restart Home Assistant.
3. Go to **Settings** -> **Devices & Services**. * **HACS (Recommended):** Add this repository as a "Custom Repository" in HACS and click Install.
4. Click **Add Integration** and search for "WhatsApp Bridge Integration". * **BCS (Bahmcloud Store):** Search for "WhatsApp Bridge" in the BCS interface.
* **Manual:** Copy the `custom_components/whatsapp_bridge_integration` folder into your `/config/custom_components/` directory and restart Home Assistant.
--- ---
## 📖 How to use ## 👥 Configuration & Profiles
Once installed, the integration provides a new service (action) that you can use in any automation. Add your WhatsApp accounts via the Home Assistant UI:
1. Go to **Settings** -> **Devices & Services**.
2. Click **Add Integration** and search for **WhatsApp Bridge**.
3. Follow the setup to add an **Account Name** (e.g., "Work") and your **Phone Number**.
4. **Repeat** this process for as many accounts as you need.
### Send a Message via YAML ### Resulting Entities
You can call the service directly in your automations: The integration automatically creates sensors for your UI:
* `sensor.whatsapp_account_name`: An entity for each individual account.
* `sensor.whatsapp_broadcast_all`: A global entity to message all accounts at once.
---
## 🛠 Usage
### Action: `send_message`
This integration registers a service/action to send messages.
**Fields:**
* **Recipient:** Select an account from the dropdown, choose the "Broadcast" entity, or manually type a phone number.
* **Message:** The text content you wish to send.
#### YAML Example:
```yaml ```yaml
action: whatsapp_bridge_integration.send_message action: whatsapp_bridge_integration.send_message
data: data:
number: "491701234567" recipient: "sensor.whatsapp_rene"
message: "The washing machine is finished! 🧺" message: "The garage door is still open!"
``` ```
### Use in Node-RED ## ⚠️ Important Notes
Add a Call Service (or Action) node.
Domain: whatsapp_bridge_integration
Service: send_message
Data:
```JSON
{
"number": "491701234567",
"message": "{{payload}}"
}
```
## 👥 Multi-Account Support (Coming Soon)
We are currently working on a UI-based configuration flow so you can save "Account Aliases" (like "Dad", "Work", "Security") and select them from a dropdown menu without typing the phone number every time.
##⚠️ Important Notes
Phone Numbers: Always use the international format without + or 00 (e.g., 49... for Germany). Phone Numbers: Always use the international format without + or 00 (e.g., 49... for Germany).
Local Network: The integration communicates locally with the Add-on on port 3000. Local Network: The integration communicates locally with the Add-on on port 3000.
@@ -60,3 +60,53 @@ Local Network: The integration communicates locally with the Add-on on port 3000
Developed by Bahmcloud Developed by Bahmcloud
--- ---
## 🤖 Node-RED Integration
The integration is fully compatible with Node-RED. Since it uses native entities, you can use the standard *Action (Call Service)* node.
### Example: Smart Window Warning
Notify yourself if a window or door has been left open for 15 minutes.
1. *Trigger*: Use a `State Trigger` or `Device` node.
2. *Logic (Change Node)*: Use JSONata to create a dynamic message:
- *Set*: msg.payload
- *Value (JSONata)*: `payload.to_state.attributes.friendly_name & " has been open for 15 minutes!"`
3. WhatsApp Node (Action Node):
- *Action*: `whatsapp_bridge_integration.send_message`
- *Data (JSON)*:
```JSON
{
"recipient": "sensor.whatsapp_broadcast_all",
"message": "{{payload}}"
}
```
### Manual Tool Calling
To send a message to a number not saved as a profile, simply pass the raw phone number to the recipient field in your Node-RED JSON:
```JSON
{
"recipient": "491701234567",
"message": "Direct alert to external number."
}
```
---
# ✨ Features
- *Dynamic UI*: Accounts are automatically added to the recipient dropdown.
- *One-Click Broadcast*: Special entity to reach all configured profiles instantly.
- *Manual Fallback*: Clean UI remains flexible; type any number to bypass profiles.
- *Developer Friendly*: Designed for easy integration with Node-RED and HA Automations.
---
Maintained by Bahmcloud

View File

@@ -1,5 +1,6 @@
import logging import logging
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -8,11 +9,51 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True return True
async def async_setup_entry(hass: HomeAssistant, entry): async def async_setup_entry(hass: HomeAssistant, entry):
"""Set up entry and forward to sensor platform.""" """Set up entry, create devices, and clean up old orphaned devices."""
# Lege die Daten im hass-Objekt ab
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = entry.data hass.data[DOMAIN][entry.entry_id] = entry.data
device_registry = dr.async_get(hass)
# 1. Automatisches Aufräumen von alten Geister-Geräten
active_entry_ids = set(hass.config_entries.async_entries(DOMAIN))
active_ids = {e.entry_id for e in active_entry_ids}
# Durchsuche die HA-Datenbank nach Geräten unserer Integration
devices_to_clean = [
dev for dev in device_registry.devices.values()
if any(identifier[0] == DOMAIN for identifier in dev.identifiers)
]
for device in devices_to_clean:
# Falls ein Gerät zu einem gelöschten Config Entry gehört -> Löschen!
# Der globale Hub ("global_bridge_hub") wird ignoriert und bleibt erhalten.
for config_id in device.config_entries:
if config_id not in active_ids and not any(idx == "global_bridge_hub" for _, idx in device.identifiers):
_LOGGER.info(f"Entferne verwaistes WhatsApp-Gerät: {device.name}")
device_registry.async_remove_device(device.id)
# 2. Eigenes Gerät SPEZIFISCH für dieses aktive Konto anlegen
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)},
name=f"WhatsApp {entry.title}",
manufacturer="Bahmcloud",
model="Kontakt-Endpunkt",
sw_version="0.2.4",
)
# 3. Zentralen System-Hub anlegen mit erzwungener config_entry_id Bindung (HA Core Anforderung)
# Nutzt Multi-Entry-Zusammenführung über den fixen "global_bridge_hub" Identifier.
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, "global_bridge_hub")},
name="WhatsApp Bridge Hub",
manufacturer="Bahmcloud",
model="Bridge Server",
sw_version="0.2.4",
)
# Forward the setup to the sensor platform to create entities # Forward the setup to the sensor platform to create entities
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"]) await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])

View File

@@ -4,7 +4,7 @@
"documentation": "https://git.bahmcloud.de/bahmcloud/Whatsapp-Bridge-Integration", "documentation": "https://git.bahmcloud.de/bahmcloud/Whatsapp-Bridge-Integration",
"dependencies": [], "dependencies": [],
"codeowners": ["@bahmcloud"], "codeowners": ["@bahmcloud"],
"version": "0.1.6", "version": "0.2.4",
"iot_class": "local_polling", "iot_class": "local_polling",
"config_flow": true "config_flow": true
} }

View File

@@ -1,22 +1,157 @@
import logging
import async_timeout
import aiohttp
from datetime import timedelta
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from .const import DOMAIN, CONF_ACCOUNT_NAME from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Add a sensor for each WhatsApp account.""" """Add sensors for WhatsApp accounts, live status, and a global broadcast sensor."""
async_add_entities([WhatsAppAccountEntity(entry)], True)
# Coordinator für den echten Live-Status vom Add-on einrichten
async def _async_update_data():
url = "http://866fd2eb-whatsapp-bridge:3000/api/status"
async with aiohttp.ClientSession() as session:
try:
async with async_timeout.timeout(5):
async with session.get(url) as response:
if response.status != 200:
raise UpdateFailed(f"API meldet Fehler {response.status}")
return await response.json()
except Exception as err:
raise UpdateFailed(f"Add-on nicht erreichbar: {err}")
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="whatsapp_bridge_status",
update_method=_async_update_data,
update_interval=SCAN_INTERVAL,
)
try:
await coordinator.async_config_entry_first_refresh()
except UpdateFailed:
_LOGGER.warning("Erster Status-Abruf fehlgeschlagen. Add-on läuft eventuell noch an.")
# Jeder Account bekommt immer seine eigene Entität
entities = [WhatsAppAccountEntity(entry)]
# Globale Entitäten (Status & Broadcast) NUR EINMALIG für die gesamte Integration anlegen
if not hass.data[DOMAIN].get("global_entities_registered"):
entities.append(WhatsAppStatusSensor(coordinator))
entities.append(WhatsAppBroadcastEntity())
hass.data[DOMAIN]["global_entities_registered"] = True
async_add_entities(entities, True)
class WhatsAppAccountEntity(SensorEntity): class WhatsAppAccountEntity(SensorEntity):
"""Representation of a WhatsApp Account as an entity.""" """Individual Account (e.g., Rene, Work)."""
def __init__(self, entry): def __init__(self, entry):
self._entry = entry self._entry = entry
self._attr_name = f"WhatsApp {entry.title}" self._attr_name = f"WhatsApp {entry.title}"
self._attr_unique_id = f"{entry.entry_id}_status" self._attr_unique_id = f"{entry.entry_id}_status"
self._attr_icon = "mdi:whatsapp" self._attr_icon = "mdi:whatsapp"
# Wir speichern die Nummer in den Attributen, damit der Service sie findet
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
"phone_number": entry.data.get("phone_number") "phone_number": entry.data.get("phone_number"),
"is_broadcast": False
} }
@property @property
def state(self): def state(self):
return "Ready" return "Ready"
@property
def device_info(self) -> DeviceInfo:
"""Ordnet diesen Sensor seinem spezifischen Personen-Gerät zu."""
return DeviceInfo(
identifiers={(DOMAIN, self._entry.entry_id)},
)
class WhatsAppBroadcastEntity(SensorEntity):
"""Virtual 'All Accounts' Entity, geordnet unter dem zentralen Hub."""
def __init__(self):
self._attr_name = "WhatsApp Broadcast (All)"
self._attr_unique_id = "whatsapp_bridge_broadcast_all"
self._attr_icon = "mdi:whatsapp-arrow-up"
self._attr_extra_state_attributes = {
"is_broadcast": True
}
@property
def state(self):
return "Global"
@property
def device_info(self) -> DeviceInfo:
"""Ordnet diese Entität dem permanenten, zentralen Hub-Gerät zu."""
return DeviceInfo(
identifiers={(DOMAIN, "global_bridge_hub")},
)
class WhatsAppStatusSensor(SensorEntity):
"""Echter Live-Status-Sensor, geordnet unter dem zentralen Hub."""
def __init__(self, coordinator):
self.coordinator = coordinator
self._attr_name = "WhatsApp Bridge Status"
self._attr_unique_id = "whatsapp_bridge_live_status_global"
self._attr_icon = "mdi:whatsapp"
@property
def should_poll(self) -> bool:
return False
@property
def available(self) -> bool:
return self.coordinator.last_update_success
@property
def state(self):
"""Gibt den dynamischen Status aus der Add-on API zurück."""
data = self.coordinator.data
if not data:
return "OFFLINE"
if data.get("isReady"):
return "CONNECTED"
elif data.get("hasQr"):
return "WAITING_FOR_SCAN"
elif data.get("isInitializing"):
return "INITIALIZING"
return "UNKNOWN"
@property
def extra_state_attributes(self):
"""Zusätzliche Attribute für erweitertes Debugging."""
data = self.coordinator.data
if data:
return {
"client_state": data.get("clientState", "UNKNOWN"),
"has_qr_code": data.get("hasQr", False)
}
return {}
@property
def device_info(self) -> DeviceInfo:
"""Ordnet diese Entität dem permanenten, zentralen Hub-Gerät zu."""
return DeviceInfo(
identifiers={(DOMAIN, "global_bridge_hub")},
)
async def async_added_to_hass(self):
"""Registrieren beim Live-Coordinator."""
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)

View File

@@ -1,43 +1,44 @@
import requests import requests
import logging import logging
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from .const import DOMAIN from .const import DOMAIN, CONF_PHONE_NUMBER
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_services(hass: HomeAssistant): async def async_setup_services(hass: HomeAssistant):
async def handle_send_message(call: ServiceCall): async def handle_send_message(call: ServiceCall):
message = call.data.get("message") message = call.data.get("message")
target_entity = call.data.get("recipient") recipient = call.data.get("recipient")
targets = [] targets = []
entries = hass.config_entries.async_entries(DOMAIN)
# 1. Fall: Broadcast an alle # 1. Prüfen, ob es eine Entity-ID ist
if target_entity == "all": if recipient.startswith("sensor."):
entities = hass.states.async_all("sensor") state = hass.states.get(recipient)
for entity in entities:
if entity.entity_id.startswith("sensor.whatsapp_"):
targets.append(entity.attributes.get("phone_number"))
# 2. Fall: Einzelne Entity ausgewählt
else:
state = hass.states.get(target_entity)
if state: if state:
num = state.attributes.get("phone_number") # Ist es die 'All Accounts' Entität?
if num: if state.attributes.get("is_broadcast"):
targets.append(num) for e in entries:
else: targets.append(e.data.get(CONF_PHONE_NUMBER))
# Fallback für manuelle Eingabe in YAML else:
targets.append(target_entity) # Es ist ein einzelner Account
targets.append(state.attributes.get("phone_number"))
# 2. Fallback: Manuelle Nummer (wenn keine sensor-ID übergeben wurde)
else:
targets.append(recipient)
# Senden
for num in targets: for num in targets:
if not num: continue
url = "http://866fd2eb-whatsapp-bridge:3000/send" url = "http://866fd2eb-whatsapp-bridge:3000/send"
try: try:
await hass.async_add_executor_job( await hass.async_add_executor_job(
lambda: requests.post(url, json={"number": num, "message": message}, timeout=10) lambda: requests.post(url, json={"number": num, "message": message}, timeout=10)
) )
_LOGGER.info("Sent to %s", num) _LOGGER.info("WhatsApp sent to %s", num)
except Exception as e: except Exception as e:
_LOGGER.error("Error: %s", str(e)) _LOGGER.error("Error sending to %s: %s", num, str(e))
hass.services.async_register(DOMAIN, "send_message", handle_send_message) hass.services.async_register(DOMAIN, "send_message", handle_send_message)

View File

@@ -4,13 +4,17 @@ send_message:
fields: fields:
recipient: recipient:
name: Recipient name: Recipient
description: Select an account entity or type a manual number. description: Select an Account, the Broadcast entity, or type a manual number.
required: true required: true
selector: selector:
text: {} # Dies zeigt nun "WhatsApp Rene", "WhatsApp Work" UND "WhatsApp Broadcast (All)" an.
entity:
filter:
integration: whatsapp_bridge_integration
domain: sensor
message: message:
name: Message name: Message
description: The content of your message. description: Your message text.
required: true required: true
selector: selector:
text: text: