Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 10 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@
- [Installation](#installation)
- [Easy way](#easy-way)
- [Manually](#manually)
- [Configuration Example](#configuration-example)
- [Supported Devices and Operations](#supported-devices-and-operations)
- [Scooper SE](#supported-devices-and-operations)
- [Scooper PRO](#supported-devices-and-operations)
- [How to Configure?](#how-to-configure)
- [Configuration (UI)](#configuration-ui)
- [API Regions](#api-regions)
- [Services (Optional)](#services-optional)
- [How to contribute?](#how-to-contribute)
Expand Down Expand Up @@ -83,25 +82,6 @@ wget -O - https://get.hacs.vip | DOMAIN=catlink REPO_PATH=hasscc/catlink ARCHIVE
3. Call this [`service: shell_command.update_catlink`](https://my.home-assistant.io/redirect/developer_call_service/?service=shell_command.update_catlink) in Developer Tools
2. Restart HA core again

### Configuration Example:

```yaml
catlink:
phone: "xxxxxx"
password: "xxxxxx"
phone_iac: 86 # Default
api_base: "https://app-usa.catlinks.cn/api/"
scan_interval: "00:00:10"
language: "en_GB"

# Multiple accounts (Optional)
accounts:
- username: 18866660001
password: password1
- username: 18866660002
password: password2
```

## Supported Devices and Operations

<div style="display: flex; justify-content: space-around;">
Expand Down Expand Up @@ -159,36 +139,18 @@ catlink:

</div>

### How to Configure?
### Configuration (UI)

> ! Recommend sharing devices to another account, because you can keep only one login session, which means that you'll have to re-login to CATLINK each time your HA instance pulls the data.

```yaml
# configuration.yaml

catlink:
# Single account
phone: xxxxxxxxx # Username of Catlink APP (without country code)
password: xxxxxxxxxx # Password
phone_iac: 86 # Optional, International access code, default is 86 (China)
api_base: # Optional, default is China server: https://app.catlinks.cn/api/ (see API Regions)
scan_interval: # Optional, default is 00:01:00
language: "en_GB"

devices: # Optional
- name: "Scooper C1" # Optional
mac: "AABBCCDDEE" # Optional
empty_weight: 3.0 # (Optional) Empty litterbox weight defaults to 0.0
max_samples_litter: 24 # (Optional) Number of samples to determinate whether cat is inside


# Multiple accounts
accounts:
- username: 18866660001
password: password1
- username: 18866660002
password: password2
```
This integration is configured entirely from the Home Assistant UI. **No configuration.yaml entry is required.**

1. Go to **Settings** → **Devices & Services** → **Add Integration**.
2. Search for **CatLink** and select it.
3. Enter your CatLINK account credentials and choose your region (see **API Regions** below).
4. Submit to finish setup.

If you have multiple accounts, repeat the steps above to add another integration entry.

#### API Regions

Expand Down
50 changes: 50 additions & 0 deletions custom_components/catlink/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The component."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.discovery import async_load_platform
Expand All @@ -20,6 +21,7 @@ async def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:
hass.data[DOMAIN].setdefault(CONF_DEVICES, {})
hass.data[DOMAIN].setdefault("coordinators", {})
hass.data[DOMAIN].setdefault("add_entities", {})
hass.data[DOMAIN].setdefault("entries", {})

component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
hass.data[DOMAIN]["component"] = component
Expand All @@ -43,4 +45,52 @@ async def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:
for platform in SUPPORTED_DOMAINS:
hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))

if config and not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.async_init(DOMAIN, context={"source": "import"}, data=config)
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up CatLink from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault("config", {})
hass.data[DOMAIN].setdefault(CONF_ACCOUNTS, {})
hass.data[DOMAIN].setdefault(CONF_DEVICES, {})
hass.data[DOMAIN].setdefault("coordinators", {})
hass.data[DOMAIN].setdefault("add_entities", {})
hass.data[DOMAIN].setdefault("entries", {})

cfg = {**entry.data, **entry.options}
if not cfg.get(CONF_PASSWORD) and not cfg.get(CONF_TOKEN):
return False

acc = Account(hass, cfg)
coordinator = DevicesCoordinator(acc)
await acc.async_check_auth()
await coordinator.async_refresh()

hass.data[DOMAIN][CONF_ACCOUNTS][acc.uid] = acc
hass.data[DOMAIN]["coordinators"][coordinator.name] = coordinator
hass.data[DOMAIN]["entries"][entry.entry_id] = acc.uid

await hass.config_entries.async_forward_entry_setups(entry, SUPPORTED_DOMAINS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
entry, SUPPORTED_DOMAINS
)
if not unload_ok:
return False

uid = hass.data[DOMAIN].get("entries", {}).pop(entry.entry_id, None)
if uid:
hass.data[DOMAIN][CONF_ACCOUNTS].pop(uid, None)
coordinator_name = f"{DOMAIN}-{uid}-{CONF_DEVICES}"
hass.data[DOMAIN]["coordinators"].pop(coordinator_name, None)
return True
6 changes: 5 additions & 1 deletion custom_components/catlink/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from .entitites import CatlinkBinaryEntity
from .helpers import Helper

async_setup_entry = Helper.async_setup_entry

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Catlink binary sensors from a config entry."""
cfg = {**config_entry.data, **config_entry.options}
await async_setup_platform(hass, cfg, async_add_entities)


async def async_setup_platform(
Expand Down
6 changes: 5 additions & 1 deletion custom_components/catlink/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from .entitites import CatlinkEntity
from .helpers import Helper

async_setup_entry = Helper.async_setup_entry

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Catlink buttons from a config entry."""
cfg = {**config_entry.data, **config_entry.options}
await async_setup_platform(hass, cfg, async_add_entities)


async def async_setup_platform(
Expand Down
98 changes: 98 additions & 0 deletions custom_components/catlink/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Config flow for CatLink integration."""

from __future__ import annotations

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv

from .const import (
CONF_API_BASE,
CONF_LANGUAGE,
CONF_PHONE,
CONF_PHONE_IAC,
CONF_SCAN_INTERVAL,
DEFAULT_API_BASE,
DOMAIN,
_LOGGER,
)
from .modules.account import Account


class CatlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for CatLink."""

VERSION = 1

async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}

if user_input is not None:
phone = user_input[CONF_PHONE]
phone_iac = user_input[CONF_PHONE_IAC]
uid = f"{phone_iac}-{phone}"
await self.async_set_unique_id(uid)
self._abort_if_unique_id_configured()

if await self._async_validate_credentials(self.hass, user_input):
return self.async_create_entry(
title=f"{phone_iac} {phone}",
data=user_input,
)
errors["base"] = "auth_failed"

data_schema = vol.Schema(
{
vol.Required(CONF_PHONE): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PHONE_IAC, default="86"): cv.string,
vol.Optional(CONF_API_BASE, default=DEFAULT_API_BASE): cv.string,
vol.Optional(CONF_LANGUAGE, default="zh_CN"): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL,
default="00:01:00",
): cv.string,
}
)

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

async def async_step_import(self, user_input: dict) -> FlowResult:
"""Handle import from YAML."""
phone = user_input.get(CONF_PHONE)
phone_iac = user_input.get(CONF_PHONE_IAC, "86")
if phone:
uid = f"{phone_iac}-{phone}"
await self.async_set_unique_id(uid)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{phone_iac} {phone}" if phone else DOMAIN,
data=user_input,
)

async def _async_validate_credentials(
self, hass: HomeAssistant, data: dict
) -> bool:
"""Validate credentials by logging in."""
try:
account = Account(hass, data)
ok = await account.async_login()
if not ok:
_LOGGER.warning(
"Config flow auth failed for %s-%s",
data.get(CONF_PHONE_IAC),
data.get(CONF_PHONE),
)
return ok
except Exception: # pragma: no cover - safety net
_LOGGER.exception("Config flow auth validation error")
return False
14 changes: 10 additions & 4 deletions custom_components/catlink/entitites/catlink.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The component."""

from homeassistant.components import persistent_notification
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from ..const import _LOGGER, DOMAIN
Expand All @@ -20,16 +20,22 @@ def __init__(self, name, device: Device, option=None) -> None:
self._device = device
self._option = option or {}
self._attr_name = f"{device.name} {name}".strip()
self._attr_device_id = f"{device.type}_{device.mac}"
device_uid = device.id or device.mac or f"{device.type}"
self._attr_device_id = f"{device_uid}"
self._attr_unique_id = f"{self._attr_device_id}-{name}"
mac = device.mac[-4:] if device.mac else device.id
self.entity_id = f"{DOMAIN}.{device.type.lower()}_{mac}_{name}"
mac = device.mac[-4:] if device.mac else device.id or device_uid
device_type = (device.type or "device").lower()
self.entity_id = f"{DOMAIN}.{device_type}_{mac}_{name}"
self._attr_icon = self._option.get("icon")
self._attr_device_class = self._option.get("class")
self._attr_unit_of_measurement = self._option.get("unit")
self._attr_state_class = option.get("state_class")
device_connections = None
if device.mac:
device_connections = {(CONNECTION_NETWORK_MAC, device.mac)}
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_device_id)},
connections=device_connections,
name=device.name,
model=device.model,
manufacturer="CatLink",
Expand Down
4 changes: 4 additions & 0 deletions custom_components/catlink/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ def calculate_update_interval(cls, update_interval_str: str) -> timedelta:

"""

if isinstance(update_interval_str, timedelta):
return update_interval_str

return (
timedelta(minutes=10)
if not update_interval_str
or not isinstance(update_interval_str, str)
or not re.match(r"^\d{2}:\d{2}:\d{2}$", update_interval_str)
else timedelta(
hours=int(update_interval_str[:2]),
Expand Down
1 change: 1 addition & 0 deletions custom_components/catlink/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"domain": "catlink",
"name": "CatLink",
"version": "0.1.1",
"config_flow": true,
"documentation": "https://github.com/hasscc/catlink",
"issue_tracker": "https://github.com/hasscc/catlink/issues",
"requirements": [],
Expand Down
Loading