Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0b723f5
Add CatLink integration base structure
Jan 6, 2026
55eacb1
Add Account module for API authentication
Jan 8, 2026
a36fa6b
Add Pydantic API models
Jan 10, 2026
335c604
Add device base classes and registry
Jan 12, 2026
8efc35a
Add litter device, litterbox and feeder
Jan 15, 2026
0f61859
Add entity base and platform entities
Jan 18, 2026
6dd34e0
Add sensor, switch, select and button platforms
Jan 20, 2026
7f7acce
Add config flow with credentials and device discovery
Jan 22, 2026
d6fa394
Add helpers and platform setup
Jan 25, 2026
b9127a4
Add documentation and project files
Feb 5, 2026
b706b47
Remove deprecated modules and fix entities folder name
Feb 5, 2026
1e9cc06
Merge pull request #4 from milosljubenovic/fix/ha-2026.2.0-compatibility
milosljubenovic Feb 5, 2026
77da198
Merge pull request #5 from milosljubenovic/v2.1.1-beta
milosljubenovic Feb 5, 2026
68a2a1c
Add pytest tests for helpers and config flow
Feb 5, 2026
45b497a
Add tests for device, entity and model classes
Feb 5, 2026
2a99e59
Add tests for FeederDevice and ScooperDevice
Feb 5, 2026
cdd3b84
Add integration setup tests
Feb 5, 2026
7250cc6
Add config flow tests for discovery, reauth, and options
Feb 5, 2026
177825b
Add platform setup tests
Feb 5, 2026
4233347
Add entity subclass tests (binary, button, select, switch)
Feb 5, 2026
1357ad0
Add LitterBox tests (garbage, last_sync, hass_sensor/select, etc.)
Feb 5, 2026
8474e5c
Add Account and DevicesCoordinator tests
Feb 5, 2026
462ff9a
Add device async methods, CatlinkSensorEntity, Account async_check_au…
Feb 5, 2026
60bc5a5
Add LogsMixin, LitterDevice, helpers, config flow, device async, and …
Feb 5, 2026
2baa609
Add GitHub Actions workflow for tests
Feb 5, 2026
c423a63
Simplifed TC's Workflow
Feb 5, 2026
25c11a8
Added missing lib
Feb 5, 2026
29f09bf
Fixed ConfigEntry Test Issue
Feb 5, 2026
3499290
Merge pull request #6 from milosljubenovic/v2.1.1-beta-tests
milosljubenovic Feb 5, 2026
ea713f1
Add reset litter and reset deodorant buttons for LitterBox
Feb 5, 2026
2c1f82c
Merge pull request #7 from milosljubenovic/feature/litterbox-reset-co…
milosljubenovic Feb 5, 2026
99fa39f
Handle device detail parsing failures with raw fallback
Feb 6, 2026
53ea3bf
Merge pull request #8 from milosljubenovic/fix/device-info-parse-fall…
milosljubenovic Feb 6, 2026
b33ca36
Add limited support for Scooper Pro Ultra device
Feb 6, 2026
aa00933
Merge pull request #9 from milosljubenovic/feature/scooper-pro-ultra-…
milosljubenovic Feb 6, 2026
964ef91
Add Open-X/C08 device support
Feb 6, 2026
6a11b4b
Merge pull request #10 from milosljubenovic/feature/c08-support
milosljubenovic Feb 6, 2026
41104cd
Initial Implementation
Feb 6, 2026
340cfc3
Set avatar entity picture
Feb 6, 2026
2661856
Add tests for entity pictures
Feb 6, 2026
9aefa48
Merge branch 'main' into feature/exposing-cat-health
milosljubenovic Feb 6, 2026
b0d867d
Added Changelog and Updated Readme.MD
Feb 6, 2026
728aaaf
Merge pull request #12 from milosljubenovic/release/readme-changelog
milosljubenovic Feb 6, 2026
4cb70f1
Merge pull request #11 from milosljubenovic/feature/exposing-cat-health
milosljubenovic Feb 6, 2026
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
28 changes: 28 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# .github/workflows/tests.yaml
name: Tests

on:
push:
branches: [main, master, v2.1.1-beta-tests]
pull_request:
branches: [main, master, v2.1.1-beta-tests]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements_test.txt

- name: Run tests
run: pytest tests/ -v --timeout=10
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## 2.1.1 - 2026-02-06

### Added
- Open-X/C08 device support (thanks to this nice repo: https://github.com/eulemitkeule/pycatlink)
- Limited Scooper Pro Ultra support
- Reset litter and reset deodorant buttons for litterbox
- Config flow coverage for discovery, reauthentication, and options
- Test suite additions and GitHub Actions workflow for tests

### Fixed
- Device detail parsing fallback when API payloads are incomplete
- Home Assistant 2026.2.0 compatibility issues

### Changed
- Device and entity organization with new helpers and logs mixin
158 changes: 70 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@
- [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)
- [API Regions](#api-regions)
- [Services (Optional)](#services-optional)
- [Changelog](#changelog)
- [How to contribute?](#how-to-contribute)
- [Disclaimer on Using Logos](#disclaimer-on-using-logos)

Expand Down Expand Up @@ -83,30 +82,11 @@ 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;">
<div style="display: flex; justify-content: space-between; flex-wrap: nowrap; gap: 16px;">

<div style="text-align: center; width: 45%;">
<div style="text-align: center; width: 33.33%;">
<h3><a href="https://www.catlinkus.com/products/catlink-smart-litter-box-scooper-se">Scooper SE</a></h3>
<img src="https://www.catlinkus.com/cdn/shop/files/CATLINK-Lite-01_757acadb-ebb8-4469-88c6-3ca3dd820706_610x610_crop_center.jpg?v=1691003577" alt="Scooper SE" width="150">
<h4>Operations</h4>
Expand All @@ -117,16 +97,18 @@ catlink:
<li>Litter weight measurement</li>
<li>Litter days left</li>
<li>Deodorant replacement countdown in days</li>
<li>(NEW) Reset litter and deodorant buttons</li>
<li>Occupacy flag</li>
<li>Cleaning count</li>
<li>Knob status</li>
<li>Garbage Tobe status</li>
<li>Online status</li>
<li>Logs & Errors</li>
<li>Entities: sensor, binary sensor, select, switch, button</li>
</ul>
</div>

<div style="text-align: center; width: 45%;">
<div style="text-align: center; width: 33.33%;">
<h3><a href="https://www.catlinkus.com/products/catlink-self-cleaning-cat-litter-box-pro">Scooper PRO</a></h3>
<img src="https://www.catlinkus.com/cdn/shop/files/1500-1500_610x610_crop_center.jpg?v=1691705114" alt="Scooper PRO" width="150">
<h4>Operations</h4>
Expand All @@ -136,95 +118,91 @@ catlink:
<li>Deodorant replacement countdown in days</li>
<li>Litter days left</li>
<li>Litter weight measurement</li>
<li>Reset litter and deodorant buttons</li>
<li>Occupacy flag</li>
<li>Cleaning count</li>
<li>Temperature (Celsius)</li>
<li>Humidity</li>
<li>Online status</li>
<li>Logs & Error</li>
<li>Entities: sensor, binary sensor, select, switch, button</li>
</ul>
</div>

<div style="text-align: center; width: 45%;">
<div style="text-align: center; width: 33.33%;">
<h3><a href="https://www.catlinkus.com/products/catlink-ai-feeder-for-only-pet-young">Feeder Young</a></h3>
<img src="https://cdn.shopify.com/s/files/1/0641/0056/5251/files/3_94db5ca7-eeeb-4f76-bd7a-c35c3a434a48_610x610_crop_center.jpg?v=1718122855" alt="Feeder Young" width="150">
<img src="https://web.archive.org/web/20221230071208im_/https://cdn.shopify.com/s/files/1/0641/0056/5251/products/3_cd58df89-6457-45af-a5c7-6ceb01272c40_700x.jpg?v=1657250711" alt="Feeder Young" width="150">
<h4>Operations</h4>
<ul style="text-align: left;">
<li>Feed Button</li>
<li>Food tray weight</li>
<li>Online status</li>
<li>Logs & Error</li>
<li>Entities: sensor, binary sensor, button</li>
</ul>
</div>

</div>

#### Additional supported devices

<div style="display: flex; justify-content: space-around;">

<div style="text-align: center; width: 45%;">
<h3>Open-X/C08</h3>
<img src="https://www.catlinkus.com/cdn/shop/files/OPENX9_610x610_crop_center.webp?v=1767769832" alt="Open-X/C08" width="150">
<h4>Operations</h4>
<ul style="text-align: left;">
<li>Changing operation mode (Auto, Manual, Scheduled)</li>
<li>Actions (Clean, Pause, Cancel, Pave)</li>
<li>Litter weight, remaining days, and deodorant countdown</li>
<li>Quiet mode, child lock, indicator light, keypad tone</li>
<li>Notice switches and pet stats</li>
<li>Entities: sensor, binary sensor, select, switch, button</li>
</ul>
</div>

<div style="text-align: center; width: 45%;">
<h3>Scooper Pro Ultra (limited support)</h3>
<img src="https://www.catlinkus.com/cdn/shop/files/ULTRA3_832ba0c1-c1b6-4ec0-ba8a-6ec5122897dd_610x610_crop_center.webp?v=1768480746" alt="Scooper Pro Ultra" width="150">
<h4>Operations</h4>
<ul style="text-align: left;">
<li>Litter remaining days</li>
<li>Deodorant countdown</li>
<li>Total clean time</li>
<li>Logs</li>
<li>Entities: sensor</li>
</ul>
</div>

</div>

#### Cats

<div style="display: flex; justify-content: space-around;">

<div style="text-align: center; width: 45%;">
<h3>Smart collars (via the Cats integration)</h3>
<img src="https://play-lh.googleusercontent.com/eHPhN_fUDhdxMK4JAvlzjB5Mh-H72crLn2U3Khk37lzolNg2CTDgZXkB5bjPiM3CDqM" alt="CatLINK smart collar" width="150">
<h4>Operations</h4>
<ul style="text-align: left;">
<li>Activity and status sensors</li>
<li>Weight and body metrics sensors</li>
<li>Presence and last seen tracking</li>
<li>Entities: sensor, binary sensor</li>
</ul>
</div>

</div>


### How to Configure?

> ! 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
```

#### API Regions

> To verify your region, please navigate to `Me` > `Settings` > `Server Nodes`

<p style="font-size: 12px; font-style:italic"> Please precise your location, as number of features might depend on it. </p>

<table style="width: 100%; border-collapse: collapse; text-align: left;">

<thead>
<tr>
<th style="padding: 8px 10px; border-bottom: 1px solid #ddd; font-weight: bold;">Region</th>
<th style="padding: 8px 10px; border-bottom: 1px solid #ddd; font-weight: bold;">API Base</th>
</tr>
</thead>

<tbody>
<tr>
<td style="padding: 8px 10px;"><span style="font-size: 20px;">🌎</span> Global/Recomended</td>
<td style="padding: 8px 10px;"><a href="https://app.catlinks.cn/api/" target="_blank" style="color: #0066cc; text-decoration: none;">https://app.catlinks.cn/api/</a></td>
</tr>
<tr>
<td style="padding: 8px 10px;"><span style="font-size: 20px;">🇨🇳</span> Mainland China (Sh)</td>
<td style="padding: 8px 10px;"><a href="https://app-sh.catlinks.cn/api/" target="_blank" style="color: #0066cc; text-decoration: none;">https://app-sh.catlinks.cn/api/</a></td>
</tr>
<tr>
<td style="padding: 8px 10px;"><span style="font-size: 20px;">🇺🇸</span> Euroamerica</td>
<td style="padding: 8px 10px;"><a href="https://app-usa.catlinks.cn/api/" target="_blank" style="color: #0066cc; text-decoration: none;">https://app-usa.catlinks.cn/api/</a></td>
</tr>
<tr>
<td style="padding: 8px 10px;"><span style="font-size: 20px;">🇸🇬</span> Singapore</td>
<td style="padding: 8px 10px;"><a href="https://app.catlinks.cn/api/" target="_blank" style="color: #0066cc; text-decoration: none;">https://app-sgp.catlinks.cn/api/</a></td>
</tr>
</tbody>

</table>
Just use ConfigFlow. Enter your phonenumber (eg. +493034994004) and password. <br>
That's it. <br>
It will automatically discover your Region, Cats & Devices.

## Services (Optional)

Expand All @@ -240,6 +218,10 @@ data:
key: val
```

## Changelog

See `CHANGELOG.md` for release notes.

### How to contribute?

Please visit [CONTRIBUTE](/CONTRIBUTE.md), and be aware of [this](/CODE_OF_CONDUCT.md).
Expand Down
1 change: 1 addition & 0 deletions custom_components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Custom components package."""
80 changes: 52 additions & 28 deletions custom_components/catlink/__init__.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,70 @@
"""The component."""

from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TOKEN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICES
from homeassistant.core import HomeAssistant
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import config_validation as cv

from .const import _LOGGER, CONF_ACCOUNTS, DOMAIN, SCAN_INTERVAL, SUPPORTED_DOMAINS
from .const import (
_LOGGER,
CONF_ACCOUNTS,
CONF_DEVICE_IDS,
DOMAIN,
SUPPORTED_DOMAINS,
)
from .modules.account import Account
from .modules.devices_coordinator import DevicesCoordinator

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)


async def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:
"""Set up the CatLink component."""

hass.data.setdefault(DOMAIN, {})
config = hass_config.get(DOMAIN) or {}
hass.data[DOMAIN]["config"] = 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("config", {})
hass.data[DOMAIN].setdefault("entry_coordinators", {})
return True


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

config = {**entry.data, **(entry.options or {})}
acc = Account(hass, config)
device_ids = entry.options.get(CONF_DEVICE_IDS) if entry.options else None
coordinator = DevicesCoordinator(acc, entry.entry_id, device_ids=device_ids)

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]["entry_coordinators"][entry.entry_id] = coordinator

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."""
await hass.config_entries.async_unload_platforms(entry, SUPPORTED_DOMAINS)

component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
hass.data[DOMAIN]["component"] = component
await component.async_setup(config)

als = config.get(CONF_ACCOUNTS) or []
if CONF_PASSWORD in config:
acc = {**config}
acc.pop(CONF_ACCOUNTS, None)
als.append(acc)
for cfg in als:
if not cfg.get(CONF_PASSWORD) and not cfg.get(CONF_TOKEN):
continue
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

for platform in SUPPORTED_DOMAINS:
hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))
uid = f"{entry.data.get('phone_iac', '86')}-{entry.data.get('phone', '')}"
if uid in hass.data[DOMAIN][CONF_ACCOUNTS]:
del hass.data[DOMAIN][CONF_ACCOUNTS][uid]
coordinator_name = f"{DOMAIN}-{uid}-{CONF_DEVICES}"
if coordinator_name in hass.data[DOMAIN]["coordinators"]:
del hass.data[DOMAIN]["coordinators"][coordinator_name]
if entry.entry_id in hass.data[DOMAIN]["entry_coordinators"]:
del hass.data[DOMAIN]["entry_coordinators"][entry.entry_id]
if entry.entry_id in hass.data[DOMAIN].get("add_entities", {}):
del hass.data[DOMAIN]["add_entities"][entry.entry_id]

return True
Loading