Skip to content

Commit

Permalink
Merge pull request #7 from sayam93/main
Browse files Browse the repository at this point in the history
feat: Add Integration through UI without breaking old config
  • Loading branch information
arifwn authored Jan 4, 2025
2 parents 86a62d5 + e44b261 commit 29ca41d
Show file tree
Hide file tree
Showing 8 changed files with 640 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.
---
50 changes: 49 additions & 1 deletion custom_components/whatspie/__init__.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
"""Send notifications via WhatsPie"""
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, CONF_API_TOKEN, CONF_FROM_NUMBER, CONF_COUNTRY_CODE

_LOGGER = logging.getLogger(__name__)


CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_TOKEN): cv.string,
vol.Required(CONF_FROM_NUMBER): cv.string,
vol.Required(CONF_COUNTRY_CODE): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA, # Allow additional keys in YAML
)


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
)
Loading

0 comments on commit 29ca41d

Please sign in to comment.