Skip to content

Commit

Permalink
feat: Add Integration through UI without breaking old config
Browse files Browse the repository at this point in the history
Allow users to add this integration using UI without breaking old configuration. Any notify service previously created through manual code in configuration.yaml remains functional. It gives the user choice to setup through UI if they want or continue with the old method.

While setup through UI, the user enters the Whatsapp number (without country code) and country code separately.

If the user's number is '+919876556789' where country code is '+91', then the user shall to enter 9876556789 as the WhatsApp Phone Number and +91 as the Country Code during setup. A notification service name notify.whatspie_919876556789 will be saved in such case.

When calling the notify service (like notify.whatspie_919876556789), the user has to mention the full phone number of the recipient with country code.

An updated readme is also available for users.
  • Loading branch information
sayam93 committed Dec 20, 2024
1 parent 86a62d5 commit 3c07fca
Show file tree
Hide file tree
Showing 8 changed files with 625 additions and 40 deletions.
119 changes: 111 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,83 @@
# homeassistant-whatspie-integration
Send Home Assistant notifications to WhatsApp using [WhatsPie](https://whatspie.com/)

Send Home Assistant notifications to WhatsApp using [WhatsPie](https://whatspie.com/).

## Features
- Send WhatsApp messages directly from Home Assistant.
- UI-based configuration for easy setup.
- Backward compatibility with YAML configuration.
- Supports sending messages to multiple recipients.
- Options flow to update configurations without reinstallation.
- Detailed error handling and logging for troubleshooting.

---

## Installation
- Copy `custom_components/whatspie` directory into Home Assistant's `config/custom_components/`
- Create a new notification service in your `configuration.yaml` file:

### Manual Installation
1. Copy the `custom_components/whatspie` directory into Home Assistant's `config/custom_components/`.
2. Restart Home Assistant.

### HACS Installation
1. Add this GitHub repository as a custom repository in HACS: [HACS Custom Repositories](https://hacs.xyz/docs/faq/custom_repositories).
2. Install the **WhatsPie** integration via HACS.
3. Restart Home Assistant.

---

## Configuration

### Configuration via UI (Recommended)
1. Go to **Settings** > **Devices & Services** in Home Assistant.
2. Click **Add Integration** and search for **WhatsPie**.
3. Enter the required details:
- **API Token**: Your WhatsPie API token.
- **From Number**: Your WhatsPie phone number (without 0 or country code).
- **Country Code**: Your country code prefix (e.g., `+62` for Indonesia).
4. Click **Submit** to complete the setup.

To modify settings, go to the **WhatsPie Integration** in the UI.

---

### YAML Configuration (Legacy)
You can still use YAML for setup if preferred. Add the following to your `configuration.yaml`:

```yaml
notify:
- name: send_wa
platform: whatspie
api_token: "<your whatspie api token>"
from_number: "<your whatspie phone number with country code prefix, e.g. 62111222333>"
country_code: "<your country code prefix, e.g. 62>"
from_number: "<your whatspie phone number with country code, e.g., 62111222333>"
country_code: "<your country code, e.g., 62>"
```
- Restart Home Assistant: Go to "Developer Tools", then press "Check Configuration" followed by "Restart"
- Alternatively, you can install this integration from HACS by adding this github repository as a custom repository: https://hacs.xyz/docs/faq/custom_repositories/
Restart Home Assistant after updating `configuration.yaml`.

---

## Usage

Example automation configuration:
### Example Automation Configuration

#### Using the Notify Service created using UI
```
alias: Send Test Notification
description: ""
trigger: []
condition: []
action:
- action: notify.whatspie_621122334455
data:
message: Test Notification -- HomeAssistant
target:
- "+621122335555"
mode: single
```
##### In target, change the number with the recepient number
#### Using the Notify Service configured using YAML
```yaml
alias: Send Test Notification
description: ""
Expand All @@ -32,3 +91,47 @@ action:
- "+621122334455"
mode: single
```

#### With Media Attachments (YAML only and if supported by WhatsPie API)
```yaml
alias: Send Media Notification
description: ""
trigger: []
condition: []
action:
- service: notify.send_wa
data:
message: "Test Notification with media"
target:
- "+621122334455"
data:
media_url: "https://example.com/path/to/image.jpg"
mode: single
```
---
## Advanced Features
### Updating Configuration created in UI via Options Flow
- Navigate to **Settings** > **Devices & Services** > **WhatsPie Integration** > **Configure**.
- Update your API token, phone number, or country code directly from the UI.
### Logging
- Check Home Assistant logs for detailed debug information in case of issues:
- API responses and errors.
- Connection issues with WhatsPie API.
---
## Troubleshooting
- **Invalid API Token**: Ensure the API token is correctly copied from your WhatsPie account.
- **Connection Issues**: Verify network connectivity and that the WhatsPie API is accessible.
- **Message Not Delivered**: Check the phone number format, including country code.
---
## Contributions
Contributions, issues, and feature requests are welcome! Please check the [issue tracker](https://github.com/arifwn/homeassistant-whatspie-integration/issues) for existing issues or create a new one.
---
34 changes: 33 additions & 1 deletion custom_components/whatspie/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
"""Send notifications via WhatsPie"""
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: ConfigType):
"""Set up the integration using YAML (deprecated)."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Whatspie integration from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = entry.data

# Forward the entry setup to the notify platform
_LOGGER.debug("Forwarding setup to notify platform for WhatsPie integration.")
await hass.config_entries.async_forward_entry_setups(entry, ["notify"])
_LOGGER.debug("Setup forwarded to notify platform successfully.")

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
await hass.config_entries.async_forward_entry_unload(entry, "notify")
hass.data[DOMAIN].pop(entry.entry_id)
return True
206 changes: 206 additions & 0 deletions custom_components/whatspie/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import voluptuous as vol
import httpx
import logging
import ssl
import certifi

from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.core import callback
from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv
from typing import Optional, Dict

from .const import (
DOMAIN,
CONF_API_TOKEN,
CONF_FROM_NUMBER,
CONF_COUNTRY_CODE,
CONF_ORIG_FROM_NUMBER,
CONF_ORIG_COUNTRY_CODE,
)

_LOGGER = logging.getLogger(__name__)


class WhatsPieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for WhatsPie integration."""

VERSION = 1

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}

if user_input is not None:
try:
# Sanitize country_code and from_number
country_code = user_input[CONF_COUNTRY_CODE].lstrip("+")
from_number = user_input[CONF_FROM_NUMBER].lstrip(
"0"
) # Remove leading "0" if present

# Reassign sanitized values back to user_input
user_input[CONF_COUNTRY_CODE] = country_code
user_input[CONF_FROM_NUMBER] = from_number

full_number = f"{country_code}{from_number}"

await self._test_credentials(
user_input[CONF_API_TOKEN],
full_number,
)

user_input[CONF_ORIG_COUNTRY_CODE] = f"+{country_code}"
user_input[CONF_ORIG_FROM_NUMBER] = user_input[CONF_FROM_NUMBER]
user_input[CONF_COUNTRY_CODE] = f"{country_code}"
user_input[CONF_FROM_NUMBER] = full_number

_LOGGER.debug("Creating entry with sanitized values: %s", user_input)

return self.async_create_entry(
title="WhatsPie Notification",
data=user_input,
)
except ValueError as err:
if "Connection error" in str(err):
errors["base"] = "connection_error"
elif "API validation error" in str(err):
errors["base"] = "auth_error"
else:
errors["base"] = "unknown_error"
except Exception:
errors["base"] = "auth_error"

data_schema = vol.Schema(
{
vol.Required(CONF_API_TOKEN): cv.string,
vol.Required(CONF_FROM_NUMBER): cv.string,
vol.Required(CONF_COUNTRY_CODE): cv.string,
}
)

return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)

async def _test_credentials(self, api_token, full_number):
"""Test the credentials by making an API call."""

url = "https://api.whatspie.com/messages"
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
}
payload = {
"receiver": full_number, # Message self
"device": full_number,
"message": "Test message from Home Assistant - WhatsPie Integration",
"type": "chat",
"simulate_typing": 0,
}

_LOGGER.debug("Testing credentials with WhatsPie API at %s", url)

def create_ssl_context():
"""Create SSL context (blocking)."""
return ssl.create_default_context(cafile=certifi.where())

ssl_context = await self.hass.async_add_executor_job(create_ssl_context)

try:
async with httpx.AsyncClient(verify=ssl_context) as client:
response = await client.post(url, json=payload, headers=headers)

if response.status_code != 200:
_LOGGER.error(
"WhatsPie API validation failed: %s - %s",
response.status_code,
response.text,
)
raise ValueError(
f"API validation error: {response.status_code} - {response.text}"
)

except httpx.RequestError as err:
_LOGGER.error("Connection error during WhatsPie validation: %s", err)
raise ValueError(f"Connection error: {err}") from err
except Exception as err:
_LOGGER.error("Unexpected error during WhatsPie validation: %s", err)
raise ValueError(f"Unexpected error: {err}") from err

_LOGGER.debug("WhatsPie credentials validated successfully.")

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Define the options flow."""
return WhatsPieOptionsFlowHandler(config_entry)


class WhatsPieOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for WhatsPie."""

def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
"""Manage the options."""
errors = {}

if user_input is not None:
# Add validation here if necessary
if not user_input[CONF_ORIG_FROM_NUMBER].isdigit():
errors["base"] = "invalid_phone_number"
elif not user_input[CONF_ORIG_COUNTRY_CODE]:
errors["base"] = "invalid_country_code"
elif not user_input[CONF_API_TOKEN]:
errors["base"] = "invalid_api_token"
else:
# Compute the updated CONF_FROM_NUMBER
full_number = f"{user_input[CONF_ORIG_COUNTRY_CODE].lstrip('+')}{user_input[CONF_ORIG_FROM_NUMBER]}"
user_input[CONF_FROM_NUMBER] = full_number
# Update entry with new options
return self.async_create_entry(title="", data=user_input)

# Fetch current values from options
api_token = self.config_entry.options.get(
CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
)
from_number = self.config_entry.options.get(
CONF_ORIG_FROM_NUMBER, self.config_entry.data.get(CONF_ORIG_FROM_NUMBER, "")
)
country_code = self.config_entry.options.get(
CONF_ORIG_COUNTRY_CODE,
self.config_entry.data.get(CONF_ORIG_COUNTRY_CODE, ""),
)

options_schema = vol.Schema(
{
vol.Required(
CONF_API_TOKEN,
default=self.config_entry.options.get(
CONF_API_TOKEN, self.config_entry.data.get(CONF_API_TOKEN)
),
): cv.string,
vol.Required(
CONF_ORIG_FROM_NUMBER,
default=self.config_entry.options.get(
CONF_ORIG_FROM_NUMBER,
self.config_entry.data.get(CONF_ORIG_FROM_NUMBER, ""),
),
): cv.string,
vol.Required(
CONF_ORIG_COUNTRY_CODE,
default=self.config_entry.options.get(
CONF_ORIG_COUNTRY_CODE,
self.config_entry.data.get(CONF_ORIG_COUNTRY_CODE, ""),
),
): cv.string,
}
)

return self.async_show_form(
step_id="init", data_schema=options_schema, errors=errors
)
8 changes: 8 additions & 0 deletions custom_components/whatspie/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constants for the Whatspie integration."""

DOMAIN = "whatspie"
CONF_API_TOKEN = "api_token"
CONF_FROM_NUMBER = "from_number"
CONF_COUNTRY_CODE = "country_code"
CONF_ORIG_FROM_NUMBER = "orig_from_number"
CONF_ORIG_COUNTRY_CODE = "orig_country_code"
Loading

0 comments on commit 3c07fca

Please sign in to comment.