diff --git a/README.md b/README.md index 774a553..8ee73b4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # green-api-custom-notifier -[green-api](https://green-api.com/en) is a service that allows us to send and receive text, photo and video using stable WhatsApp API gateway. The service includes free account that can be used to send notifications to 3 chats (Group or Private) and many more. +[green-api](https://green-api.com/en) is a service that allows us to send and receive text, photo and video using stable WhatsApp API gateway. The service includes a free account that can be used to send notifications to 3 chats (Group or Private) and many more. - -[green-api-custom-notifier](https://github.com/t0mer/green-api-custom-notifier) is a [Homeassistant ](https://www.home-assistant.io/) custom notification component that enables us to send notification to Whatsapp groups using [green-api](https://green-api.com/en). +[green-api-custom-notifier](https://github.com/t0mer/green-api-custom-notifier) is a [Home Assistant](https://www.home-assistant.io/) custom notification component that enables sending notifications to WhatsApp groups and contacts using [green-api](https://green-api.com/en). ## Limitations @@ -13,7 +12,7 @@ ## Getting started ### Setup Green API account -Nevigate to [https://green-api.com/en](https://green-api.com/en) and register for a new account: +Navigate to [https://green-api.com/en](https://green-api.com/en) and register for a new account: ![Register](screenshots/register.png) Fill up your details and click on **Register**: @@ -28,92 +27,100 @@ Select the "Developer" instance (Free): ![Developer Instance](screenshots/developer_instance.png) -Copy the InstanceId and Token, we need it for the integration settings: +Copy the InstanceId and Token — you will need these during integration setup: ![Instance Details](screenshots/instance_details.png) -Next, Lets connect our whatsapp with green-api. On the left side, Under API --> Account, click on QR and copy the QR URL to the browser and click on "Scan QR code" +Next, connect your WhatsApp with Green API. On the left side, under API → Account, click on QR and copy the QR URL to the browser and click on "Scan QR code": ![Send QR](screenshots/send_qr.png) ![Scan QR](screenshots/scan_qr.png) -Next, Scan the QR code to link you whatsapp with Green API: +Scan the QR code to link your WhatsApp with Green API: ![QR Code](screenshots/qr.png) -After the account link, you will notice that the instance is active by the green light in the instance header: +After linking, the instance will show as active with a green light in the instance header: ![Active Instance](screenshots/active_instance.png) - -### Getting the Contacts and Groups -Before we can start messaging, we need to get the Contact/Group details. we can do it using Green API endpoint. -On the lef side, Under API --> Service methods, click on "getContacts" and then click "Send": +### Getting Contacts and Groups +Before messaging, get the Contact/Group IDs via the Green API endpoint. +On the left side, under API → Service methods, click on "getContacts" and then click "Send": ![Get Contacts](screenshots/get_contacts.png) -As a result, you will get the list of Contacts and Groups. -* The contact number ends with **@c.us** -* The group number ends with **@g.us** +You will get a list of contacts and groups: +* Contact numbers end with **@c.us** +* Group numbers end with **@g.us** ![Contacts Lists](screenshots/contacts_list.png) -Write down the Id, you will need it to configure the notification. +Note the ID — you will need it when sending notifications. -### Setting up the notification in Home Assistant +### Installing the integration -Download the [green-api-custom-notifier](https://github.com/t0mer/green-api-custom-notifier), place it under the **custom_components** folder. -Restart Home Assistant and add the following section to your *configuration.yaml* file: +Download the [green-api-custom-notifier](https://github.com/t0mer/green-api-custom-notifier) and place the `greenapi` folder under your `custom_components` directory. Restart Home Assistant. +### Configuring via UI -```yaml -notify: - - platform: greenapi - name: greenapi - instance_id: #REQUIRED: Set the instanceid - token: #REQUIRED: Set the greenapi token. - target: #OPTIONAL! Set the detault target. If you set the default target here, you won't have to specify it again in your service calls. -``` +Go to **Settings → Integrations → Add Integration** and search for **GreenAPI**. Fill in the three fields: -* instance_id is the Green API instance id. -* token is the Green API instance token. -* target is the chat/contact/group id to send the message to: - * For groups, the id should end with *@g.us* - * For chats, the id should end with *@c.us* +| Field | Description | +|-------|-------------| +| **Service Name** | A short friendly name (e.g. `home`). The notify service will be registered as `notify.greenapi_`. | +| **Instance ID** | Your Green API Instance ID. | +| **Access Token** | Your Green API token. | + +You can add multiple instances (e.g. for different WhatsApp accounts) — each gets its own uniquely named service. ## Sending a message -To Send a message you call the service and provide the following parameters: -* message (**Required**): Test to send. -* title (**OPTIONAL**): Add a title for the message in **bold**. -* target (**OPTIONAL** if you've already defined the default target in your notify service, otherwise required): The chat/group id to send the message to. -![Send text message](screenshots/text_message.png) +Call the service with the following parameters: + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `message` | Yes | Text to send. | +| `title` | No | Prepended to the message in **bold**. | +| `target` | Yes | The WhatsApp chat/group ID to send to. | + +```yaml +action: notify.greenapi_home +data: + message: Hello from Home Assistant + target: 972XXXXXXXXX@c.us +``` + +With a title: +```yaml +action: notify.greenapi_home +data: + message: Motion detected in the garden + title: Security Alert + target: 972XXXXXXXXX@c.us +``` -Or from Yaml mode: +For groups, the target ID ends with `@g.us`: ```yaml -service: notify.greenapi +action: notify.greenapi_home data: - message: New Whatsapp component - target: 972*********@c.us + message: Dinner is ready! + target: 120363XXXXXXXXX@g.us ``` -### Optional - Attach media to message -To send message with media, add the following to the data parameter: -* file : [Path to the file] -![Send media](screenshots/send_media.png) +### Optional — Attach media to message + +Add a `file` key inside the `data` field to attach a local file: -Or from Yaml mode: ```yaml -service: notify.greenapi +action: notify.greenapi_home data: - message: New Whatsapp component - target: 972*********@c.us + message: Here is your image + target: 972XXXXXXXXX@c.us data: file: /config/images/Capture.png - ``` -#### Important -If the path to the file does not exist, the message will still be sent; but will log a warning. \ No newline at end of file +> **Note:** If the file path does not exist, the message is still sent as text and a warning is logged. diff --git a/custom_components/greenapi/__init__.py b/custom_components/greenapi/__init__.py index 8d1c8b6..ca7ccd8 100644 --- a/custom_components/greenapi/__init__.py +++ b/custom_components/greenapi/__init__.py @@ -1 +1,135 @@ - +"""GreenAPI WhatsApp notifier — setup and teardown.""" + +from __future__ import annotations + +import functools +import logging +import os +import re +from dataclasses import dataclass +from os.path import basename +from urllib.parse import urlparse + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, ServiceCall + +from .const import CONF_INSTANCE_ID, CONF_NAME, CONF_TOKEN, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +_NOTIFY_DOMAIN = "notify" + +_SERVICE_SCHEMA = vol.Schema( + { + vol.Required("message"): cv.string, + vol.Optional("title"): cv.string, + vol.Optional("target"): cv.string, + vol.Optional("data"): dict, + } +) + + +def _slugify(text: str) -> str: + return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_") + + +@dataclass +class GreenAPIData: + """Runtime data stored on the config entry.""" + + instance_id: str + token: str + service_name: str + + +type GreenAPIConfigEntry = ConfigEntry[GreenAPIData] + + +async def async_setup_entry(hass: HomeAssistant, entry: GreenAPIConfigEntry) -> bool: + """Set up GreenAPI from a config entry.""" + from whatsapp_api_client_python import API # noqa: PLC0415 + + data = {**entry.data, **entry.options} + + instance_id: str = data[CONF_INSTANCE_ID] + token: str = data[CONF_TOKEN] + name: str = data.get(CONF_NAME) or instance_id + service_name: str = f"greenapi_{_slugify(name)}" + + entry.runtime_data = GreenAPIData( + instance_id=instance_id, + token=token, + service_name=service_name, + ) + + greenapi = API.GreenAPI(instance_id, token) + + async def handle_send_message(call: ServiceCall) -> None: + message: str = call.data["message"] + title: str | None = call.data.get("title") + extra_data: dict | None = call.data.get("data") + + if title: + message = f"*{title}*\n{message}" + + dest: str | None = call.data.get("target") + if not dest: + _LOGGER.error("No target specified — pass 'target' in the service call data") + return + + _LOGGER.info("Sending message to %s", dest) + + try: + if extra_data: + file_path = extra_data.get("file") + if file_path: + if os.path.exists(file_path): + upload_response = await hass.async_add_executor_job( + greenapi.sending.uploadFile, file_path + ) + if upload_response.code != 200: + raise Exception( + upload_response.code, + f"Failed to upload file: {file_path}", + ) + url_file = upload_response.data["urlFile"] + file_name = basename(urlparse(url_file).path) + await hass.async_add_executor_job( + functools.partial( + greenapi.sending.sendFileByUrl, + dest, + url_file, + file_name, + caption=message, + ) + ) + return + else: + _LOGGER.warning( + "Sending message to %s: file '%s' not found, sending text only", + dest, + file_path, + ) + + await hass.async_add_executor_job( + functools.partial( + greenapi.sending.sendMessage, dest, message, linkPreview=False + ) + ) + except Exception as e: + _LOGGER.error("Failed to send message to %s: %s", dest, e) + + hass.services.async_register( + _NOTIFY_DOMAIN, service_name, handle_send_message, schema=_SERVICE_SCHEMA + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: GreenAPIConfigEntry) -> bool: + """Unload a config entry.""" + hass.services.async_remove(_NOTIFY_DOMAIN, entry.runtime_data.service_name) + return True diff --git a/custom_components/greenapi/config_flow.py b/custom_components/greenapi/config_flow.py new file mode 100644 index 0000000..64752e0 --- /dev/null +++ b/custom_components/greenapi/config_flow.py @@ -0,0 +1,41 @@ +"""Config flow for GreenAPI WhatsApp notifier.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol +from homeassistant import config_entries + +from .const import CONF_INSTANCE_ID, CONF_NAME, CONF_TOKEN, DOMAIN + +_SETUP_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_INSTANCE_ID): str, + vol.Required(CONF_TOKEN): str, + } +) + + +class GreenAPIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle the initial setup wizard.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.ConfigFlowResult: + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_INSTANCE_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=_SETUP_SCHEMA, + ) diff --git a/custom_components/greenapi/const.py b/custom_components/greenapi/const.py new file mode 100644 index 0000000..6631241 --- /dev/null +++ b/custom_components/greenapi/const.py @@ -0,0 +1,4 @@ +DOMAIN = "greenapi" +CONF_INSTANCE_ID = "instance_id" +CONF_TOKEN = "token" +CONF_NAME = "name" diff --git a/custom_components/greenapi/manifest.json b/custom_components/greenapi/manifest.json index afd46e2..f7752b1 100644 --- a/custom_components/greenapi/manifest.json +++ b/custom_components/greenapi/manifest.json @@ -1,6 +1,6 @@ { "domain": "greenapi", - "name": "Whatsapp notifier basde on Green API", + "name": "Whatsapp notifier based on Green API", "codeowners": [ "@t0mer" ], @@ -8,5 +8,7 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/t0mer/green-api-custom-notifier", "requirements": ["whatsapp-api-client-python"], - "version": "0.4.0" + "version": "0.5.0", + "integration_type": "service", + "config_flow": true } diff --git a/custom_components/greenapi/notify.py b/custom_components/greenapi/notify.py index c03ccb7..51d4c16 100644 --- a/custom_components/greenapi/notify.py +++ b/custom_components/greenapi/notify.py @@ -1,72 +1,94 @@ -import logging -import json -import os -from os.path import basename -from urllib.parse import urlparse -import voluptuous as vol -from whatsapp_api_client_python import API -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) - -ATTR_INSTANCE = "instance_id" -ATTR_TOKEN = "token" - - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(ATTR_TARGET): cv.string, - vol.Required(ATTR_INSTANCE): cv.string, - vol.Required(ATTR_TOKEN): cv.string, - vol.Optional(ATTR_TITLE): cv.string, -}) - -def get_service(hass, config, discovery_info=None): - """Get the custom notifier service.""" - title = config.get(ATTR_TITLE) - token = config.get(ATTR_TOKEN) - instance_id = config.get(ATTR_INSTANCE) - target = config.get(ATTR_TARGET) - return GreenAPINotificationService(title, token, instance_id, target) - -class GreenAPINotificationService(BaseNotificationService): - - def __init__(self, title, token, instance_id, target): - """Initialize the service.""" - self._title = title - self._token = token - self._instance_id = instance_id - self._target = target - self._greenAPI = API.GreenAPI(self._instance_id, self._token) - - def send_message(self, message="", **kwargs): - - """Send a message to the target.""" - - try: - title = kwargs.get(ATTR_TITLE) - if title is not None: - title = f"*{title}*" - message = f"{title}\n{message}" - data = kwargs.get(ATTR_DATA) - target = kwargs.get(ATTR_TARGET)[0] if kwargs.get(ATTR_TARGET) is not None else self._target #Allow setting the target from either the service-call or the service config. Service call target can override the default config. - _LOGGER.info(f"Sending message to {target}") - if data is not None: - file_path = data.get("file") - if file_path is not None: - if os.path.exists(file_path): - upload_file_response = self._greenAPI.sending.uploadFile(file_path) - if upload_file_response.code != 200: - raise Exception(upload_file_response.code, "Failed to upload file: " + file_path) - url_file = upload_file_response.data["urlFile"] - url = urlparse(url_file) - file_name = basename(url.path) - send_file_by_url_response = self._greenAPI.sending.sendFileByUrl(target, url_file, file_name, caption=message) - return - else: - _LOGGER.warn("Sending message to %s: excluding the file '%s' that was not found", kwargs.get(ATTR_TARGET)[0], file_path) - self._greenAPI.sending.sendMessage(target, message, linkPreview=False) - except Exception as e: - _LOGGER.error("Sending message to %s: has failed with the following error %s", kwargs.get(ATTR_TARGET)[0], str(e)) - +"""GreenAPI WhatsApp notification service (legacy YAML path).""" + +from __future__ import annotations + +import logging +import os +from os.path import basename +from urllib.parse import urlparse + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + PLATFORM_SCHEMA, + BaseNotificationService, +) + +from .const import CONF_INSTANCE_ID, CONF_TOKEN + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(ATTR_TARGET): cv.string, + vol.Required(CONF_INSTANCE_ID): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(ATTR_TITLE): cv.string, + } +) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the GreenAPI notification service (YAML path).""" + from whatsapp_api_client_python import API # noqa: PLC0415 + + return GreenAPINotificationService( + token=config[CONF_TOKEN], + instance_id=config[CONF_INSTANCE_ID], + target=config.get(ATTR_TARGET), + api=API, + ) + + +class GreenAPINotificationService(BaseNotificationService): + + def __init__(self, token: str, instance_id: str, target: str | None, api) -> None: + self._token = token + self._instance_id = instance_id + self._target = target + self._greenAPI = api.GreenAPI(self._instance_id, self._token) + + def send_message(self, message: str = "", **kwargs) -> None: + """Send a message to the target.""" + try: + title = kwargs.get(ATTR_TITLE) + if title is not None: + message = f"*{title}*\n{message}" + + targets = kwargs.get(ATTR_TARGET) + target = targets[0] if targets is not None else self._target + + _LOGGER.info("Sending message to %s", target) + + data = kwargs.get(ATTR_DATA) + if data is not None: + file_path = data.get("file") + if file_path is not None: + if os.path.exists(file_path): + upload_response = self._greenAPI.sending.uploadFile(file_path) + if upload_response.code != 200: + raise Exception( + upload_response.code, + f"Failed to upload file: {file_path}", + ) + url_file = upload_response.data["urlFile"] + file_name = basename(urlparse(url_file).path) + self._greenAPI.sending.sendFileByUrl( + target, url_file, file_name, caption=message + ) + return + else: + _LOGGER.warning( + "Sending message to %s: file '%s' not found, sending text only", + target, + file_path, + ) + + self._greenAPI.sending.sendMessage(target, message, linkPreview=False) + + except Exception as e: + _LOGGER.error("Failed to send message to %s: %s", target, e) diff --git a/custom_components/greenapi/strings.json b/custom_components/greenapi/strings.json new file mode 100644 index 0000000..92a5e98 --- /dev/null +++ b/custom_components/greenapi/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "GreenAPI WhatsApp Notifier", + "description": "Enter your [Green API](https://green-api.com/en) credentials. You can find these in your Green API dashboard.", + "data": { + "name": "Service Name", + "instance_id": "Instance ID", + "token": "Access Token" + }, + "data_description": { + "name": "A friendly name for this account. The service will be registered as notify.greenapi_. E.g. 'home' becomes notify.greenapi_home.", + "instance_id": "The Instance ID from your Green API dashboard.", + "token": "The API token from your Green API dashboard." + } + } + }, + "abort": { + "already_configured": "This Green API instance is already configured." + } + } +} diff --git a/custom_components/greenapi/translations/en.json b/custom_components/greenapi/translations/en.json new file mode 100644 index 0000000..92a5e98 --- /dev/null +++ b/custom_components/greenapi/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "GreenAPI WhatsApp Notifier", + "description": "Enter your [Green API](https://green-api.com/en) credentials. You can find these in your Green API dashboard.", + "data": { + "name": "Service Name", + "instance_id": "Instance ID", + "token": "Access Token" + }, + "data_description": { + "name": "A friendly name for this account. The service will be registered as notify.greenapi_. E.g. 'home' becomes notify.greenapi_home.", + "instance_id": "The Instance ID from your Green API dashboard.", + "token": "The API token from your Green API dashboard." + } + } + }, + "abort": { + "already_configured": "This Green API instance is already configured." + } + } +}