diff --git a/.gitignore b/.gitignore
index fc7ae7d..45ab9e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,4 +130,9 @@ dmypy.json
# hacs
-.local/
\ No newline at end of file
+.local/
+
+MQTT.md
+
+# Copilot instructions
+.github/copilot-instructions.md
\ No newline at end of file
diff --git a/README.md b/README.md
index 8177a59..b9e1b31 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,66 @@
# Tinxy Home Assistant Integration
-Integrate [Tinxy.in](https://tinxy.in/) smart switches and devices with Home Assistant using this custom component.
+> **Important Notice**
+> Users of the Tinxy cloud integration must update to version **5.0.0** before **10th March 2026**.
+> Older versions will stop working after this date. Please update via HACS or manually at the earliest.
-Join [Discord server](https://discord.gg/VH4jgz2f) for support.
+A custom Home Assistant integration for [Tinxy.in](https://tinxy.in/) smart devices.
+Uses MQTT push for real-time state updates and control with no polling and no delays.
-## Overview
+---
-This repository provides a Home Assistant integration for Tinxy smart devices, allowing you to control switches, fans, lights, and locks directly from Home Assistant.
+## Community
+
+Have questions, need help, or want to share feedback? Join the Tinxy community on Discord.
+
+**[Join Discord](https://discord.gg/cSPwkkQg)**
+
+---
+
+## Supported Devices
+
+- Switches
+- Lights (including dimmer)
+- Fans (with speed presets: Low / Medium / High)
+- Locks
+
+---
## Installation
-### 1. Install HACS
-HACS is a community app store for Home Assistant. Follow the [HACS setup guide](https://hacs.xyz/docs/setup/prerequisites) to install.
-
-### 2. Add Repository to HACS
-Once HACS is installed:
-1. Navigate to **HACS → Integrations**.
-
-2. Click the three dots at the top right and select **Custom repositories**.
-
-3. Paste `https://github.com/arevindh/ha-tinxy-cloud`, select **Integration**, and click **Add**.
-
-4. Close the Custom repositories section.
-5. Click **Explore & Download** at the bottom right.
- 
-6. Search for `Tinxy`, then add and install it.
-7. Restart Home Assistant.
-
-## Usage
-
-1. **Get API Key:**
- - Obtain your API key from the Tinxy mobile application.
-2. **Configure Integration:**
- - Go to **Settings → Integrations** in Home Assistant.
- - Search for "Tinxy" and click on it.
- - 
- - Enter your API key and click **Submit**.
- - 
- - Click **Finish** on the next screen.
- - You can find all devices in the integration screen.
+### HACS (Recommended)
+
+This integration is available in the HACS default store.
+
+1. Open **HACS** in Home Assistant and go to **Integrations**.
+2. Search for **Tinxy** and click **Download**.
+3. Restart Home Assistant.
+
+### Manual
+
+Copy the `custom_components/tinxy` folder into your Home Assistant `custom_components` directory and restart.
+
+---
## Configuration
-- You can adjust the `scan_interval` parameter to change how often device status is updated. It is recommended to keep this above `7` seconds to avoid slowing down your Home Assistant server.
+1. In Home Assistant, go to **Settings → Devices & Services → Add Integration**.
+2. Search for **Tinxy**.
+3. Enter your API key from the Tinxy mobile app and click **Submit**.
+
+All paired Tinxy devices will be discovered and added automatically.
-## Known Issues
+---
-- Due to response delays from the Tinxy API, toggling devices may have a delay of approximately 3 seconds.
+## Local Control
+
+A local-network alternative is available at [arevindh/tinxylocal](https://github.com/arevindh/tinxylocal) for users who prefer not to use the cloud. Note that the local integration supports only a limited number of older device models. **EVA series devices are not supported** by the local integration.
+
+---
## License
See [LICENSE](LICENSE) for details.
+
diff --git a/custom_components/tinxy/__init__.py b/custom_components/tinxy/__init__.py
index 1307266..1fa6db8 100644
--- a/custom_components/tinxy/__init__.py
+++ b/custom_components/tinxy/__init__.py
@@ -13,9 +13,10 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN, TINXY_BACKEND
-from .tinxycloud import TinxyCloud, TinxyHostConfiguration
+from .const import DOMAIN, TINXY_BACKEND, CONF_MQTT_USERNAME, CONF_MQTT_PASSWORD
+from .tinxycloud import TinxyCloud, TinxyHostConfiguration, TinxyMQTTCredentials
from .coordinator import TinxyUpdateCoordinator
+from .mqtt_client import TinxyMQTTClient
# Logger for this module
LOGGER = logging.getLogger(__name__)
@@ -26,6 +27,7 @@
Platform.LIGHT,
Platform.FAN,
Platform.LOCK,
+ Platform.SENSOR,
]
async def async_setup_entry(
@@ -47,29 +49,76 @@ async def async_setup_entry(
# Ensure domain data exists
hass.data.setdefault(DOMAIN, {})
- # Create web session for API communication
+ # Create web session for REST API communication
web_session = async_get_clientsession(hass)
- # Prepare host configuration for TinxyCloud
+ # Prepare host configuration
host_config = TinxyHostConfiguration(
api_token=entry.data[CONF_API_KEY],
api_url=TINXY_BACKEND,
)
- # Initialize TinxyCloud API
+ # Initialize REST API client and fetch device list (done once at startup)
api = TinxyCloud(host_config=host_config, web_session=web_session)
try:
await api.sync_devices()
- LOGGER.info("Successfully synced Tinxy devices for entry_id=%s", entry.entry_id)
+ LOGGER.info("Successfully synced %d Tinxy devices.", len(api.devices))
except Exception as exc:
LOGGER.error("Failed to sync Tinxy devices: %s", exc)
return False
- # Create update coordinator for device state management
+ # Create coordinator (MQTT push-driven; performs one REST fetch on first_refresh)
coordinator = TinxyUpdateCoordinator(hass, api)
- # Store API and coordinator in hass data
- hass.data[DOMAIN][entry.entry_id] = (api, coordinator)
+ # Collect unique physical device IDs (strip the "-relay_no" suffix)
+ unique_device_ids: list[str] = list(
+ {d["device_id"] for d in api.list_all_devices()}
+ )
+
+ # Create MQTT client wired to coordinator's push-update method
+ # Credentials are cached in entry.data to avoid an API call on every startup.
+ # The fetcher uses the cache on the first call and force-refreshes on auth failures.
+ _cred_fetched_once = False
+
+ async def _get_mqtt_credentials() -> TinxyMQTTCredentials:
+ nonlocal _cred_fetched_once
+ if not _cred_fetched_once and CONF_MQTT_USERNAME in entry.data:
+ _cred_fetched_once = True
+ LOGGER.debug("Tinxy: using cached MQTT credentials.")
+ return TinxyMQTTCredentials(
+ username=entry.data[CONF_MQTT_USERNAME],
+ password=entry.data[CONF_MQTT_PASSWORD],
+ )
+ # Fetch fresh credentials from the API (first run or after auth failure)
+ creds = await api.async_get_mqtt_credentials()
+ hass.config_entries.async_update_entry(
+ entry,
+ data={
+ **entry.data,
+ CONF_MQTT_USERNAME: creds.username,
+ CONF_MQTT_PASSWORD: creds.password,
+ },
+ )
+ _cred_fetched_once = True
+ LOGGER.debug("Tinxy: MQTT credentials fetched and cached.")
+ return creds
+
+ mqtt_client = TinxyMQTTClient(
+ hass=hass,
+ on_state_update=coordinator.async_update_from_mqtt,
+ credentials_fetcher=_get_mqtt_credentials,
+ )
+ # Give the REST client a reference so set_device_state() uses MQTT
+ api.mqtt_client = mqtt_client
+
+ # Start MQTT connection in the background
+ await mqtt_client.async_start(unique_device_ids)
+ LOGGER.info(
+ "Tinxy MQTT client started for %d physical devices.", len(unique_device_ids)
+ )
+
+ # Store references for platforms and unload
+ hass.data[DOMAIN][entry.entry_id] = (api, coordinator, mqtt_client)
# Forward setup to supported platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -93,9 +142,16 @@ async def async_unload_entry(
bool: True if unload was successful, False otherwise.
"""
LOGGER.info("Unloading Tinxy integration for entry_id=%s", entry.entry_id)
+
+ # Stop MQTT client before unloading platforms
+ entry_data = hass.data[DOMAIN].get(entry.entry_id)
+ if entry_data and len(entry_data) >= 3:
+ mqtt_client: TinxyMQTTClient = entry_data[2]
+ await mqtt_client.async_stop()
+ LOGGER.info("Tinxy MQTT client stopped.")
+
unload_ok: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
- # Remove stored API and coordinator
hass.data[DOMAIN].pop(entry.entry_id, None)
LOGGER.info("Tinxy integration unloaded for entry_id=%s", entry.entry_id)
else:
diff --git a/custom_components/tinxy/const.py b/custom_components/tinxy/const.py
index 1a6f626..08d82a5 100644
--- a/custom_components/tinxy/const.py
+++ b/custom_components/tinxy/const.py
@@ -3,3 +3,9 @@
DOMAIN = "tinxy"
COORDINATOR = "coordinator"
TINXY_BACKEND = "https://ha-backend.tinxy.in/"
+MQTT_SERVER = "mqtt.tinxy.in"
+MQTT_PORT = 1883
+
+# Config entry keys for persisted MQTT credentials
+CONF_MQTT_USERNAME = "mqtt_username"
+CONF_MQTT_PASSWORD = "mqtt_password"
\ No newline at end of file
diff --git a/custom_components/tinxy/coordinator.py b/custom_components/tinxy/coordinator.py
index a351a7d..b8fc9b0 100644
--- a/custom_components/tinxy/coordinator.py
+++ b/custom_components/tinxy/coordinator.py
@@ -1,100 +1,141 @@
"""
Coordinator for Tinxy Home Assistant integration.
-Handles periodic data updates from the Tinxy API and manages device status lookup.
-Follows Home Assistant best practices for custom components.
+All device state is delivered via MQTT push only. At startup every relay is
+initialised with safe defaults; the broker's retained /info messages (buffered
+before coordinator.data is ready) are replayed on top immediately after,
+giving entities their correct last-known state.
"""
-from datetime import timedelta
import logging
-from typing import Any, Dict
-import async_timeout
+from typing import Any, Dict, List, Tuple
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
-from homeassistant.helpers.debounce import Debouncer
-from .tinxycloud import TinxyAuthenticationException, TinxyException
+from .tinxycloud import TinxyException
# Module-level logger
_LOGGER = logging.getLogger(__name__)
-# Constants for configuration
-UPDATE_INTERVAL_SECONDS: int = 7
-REQUEST_REFRESH_DELAY: float = 0.35
-API_TIMEOUT_SECONDS: int = 10
-
-
class TinxyUpdateCoordinator(DataUpdateCoordinator):
"""
- Coordinates updates from the Tinxy API for Home Assistant entities.
+ Coordinates device state for Home Assistant entities.
- Periodically fetches device status and merges device info for fast entity lookup.
- Handles authentication and API errors gracefully.
+ * First refresh - builds the state dict from the device list with safe defaults.
+ * Retained MQTT /info messages that arrive before the first refresh completes
+ are buffered and replayed on top of the defaults.
+ * Ongoing updates - driven entirely by MQTT pushes; no polling timer is set.
"""
def __init__(self, hass: HomeAssistant, api_client: Any) -> None:
"""
- Initialize the TinxyUpdateCoordinator.
+ Initialize the coordinator.
Args:
- hass (HomeAssistant): The Home Assistant instance.
- api_client (Any): The Tinxy API client instance.
+ hass: The HomeAssistant instance.
+ api_client: TinxyCloud REST client instance.
"""
super().__init__(
hass,
_LOGGER,
name="Tinxy",
- update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
- request_refresh_debouncer=Debouncer(
- hass,
- _LOGGER,
- cooldown=REQUEST_REFRESH_DELAY,
- immediate=False,
- ),
+ update_interval=None, # MQTT push-driven – no polling
)
self.hass: HomeAssistant = hass
self.api_client: Any = api_client
- # Fetch all devices once at startup; assumes device list is static
+ # Static device list populated after sync_devices(); treated as immutable.
self.all_devices: list[dict] = self.api_client.list_all_devices()
+ # Buffer for MQTT messages that arrive before coordinator.data is ready.
+ self._mqtt_buffer: List[Tuple[str, Dict[str, Any]]] = []
+
+ # ------------------------------------------------------------------
+ # DataUpdateCoordinator interface
+ # ------------------------------------------------------------------
async def _async_update_data(self) -> Dict[str, dict]:
"""
- Fetch and merge device status from the Tinxy API.
+ Build initial state from the device list with safe defaults.
+ Replays any buffered MQTT retained messages on top so the very first
+ state shown to the user reflects the broker's last-known device state.
Returns:
- Dict[str, dict]: Mapping of device IDs to merged device info and status.
+ Dict mapping composite relay IDs to device + state dicts.
Raises:
- ConfigEntryAuthFailed: If authentication fails.
- UpdateFailed: For other API errors.
+ UpdateFailed: If the device list is unexpectedly empty.
"""
try:
- async with async_timeout.timeout(API_TIMEOUT_SECONDS):
- status_by_id: Dict[str, dict] = {}
- # Fetch latest status for all devices
- status_result: dict = await self.api_client.get_all_status()
-
- for device in self.all_devices:
- device_id = device.get("id")
- if device_id in status_result:
- # Merge static device info with dynamic status
- status_by_id[device_id] = {**device, **status_result[device_id]}
- else:
- # Log warning if device status is missing
- _LOGGER.warning(f"No status found for device ID: {device_id}")
-
- _LOGGER.debug(f"Fetched status for {len(status_by_id)} devices.")
- return status_by_id
- except TinxyAuthenticationException as auth_err:
- _LOGGER.error("Authentication failed with Tinxy API: %s", auth_err)
- raise ConfigEntryAuthFailed from auth_err
- except TinxyException as api_err:
- _LOGGER.error("Error communicating with Tinxy API: %s", api_err)
- raise UpdateFailed(f"Error communicating with API: {api_err}") from api_err
+ status_by_id: Dict[str, dict] = {}
+
+ for device in self.all_devices:
+ relay_id = device.get("id")
+ if relay_id:
+ status_by_id[relay_id] = {
+ **device,
+ "state": 0,
+ "status": 0,
+ "brightness": 0,
+ }
+
+ if not status_by_id:
+ raise UpdateFailed("No devices found - check API key and device setup.")
+
+ # Replay buffered MQTT retained messages over the defaults.
+ if self._mqtt_buffer:
+ _LOGGER.debug(
+ "Tinxy: replaying %d buffered MQTT messages.", len(self._mqtt_buffer)
+ )
+ for relay_id, state_dict in self._mqtt_buffer:
+ if relay_id in status_by_id:
+ status_by_id[relay_id] = {**status_by_id[relay_id], **state_dict}
+ self._mqtt_buffer.clear()
+
+ _LOGGER.info(
+ "Tinxy: initialised state for %d relays (MQTT retained messages will follow).",
+ len(status_by_id),
+ )
+ return status_by_id
+
+ except UpdateFailed:
+ raise
+ except TinxyException as exc:
+ raise UpdateFailed(f"Tinxy error during setup: {exc}") from exc
except Exception as exc:
- _LOGGER.exception("Unexpected error during Tinxy data update: %s", exc)
+ _LOGGER.exception("Tinxy: unexpected error during setup: %s", exc)
raise UpdateFailed(f"Unexpected error: {exc}") from exc
+
+ # ------------------------------------------------------------------
+ # MQTT push interface
+ # ------------------------------------------------------------------
+
+ def async_update_from_mqtt(self, relay_id: str, state_dict: Dict[str, Any]) -> None:
+ """
+ Merge MQTT state into the coordinator's data store and notify listeners.
+
+ If coordinator.data is not yet populated (first refresh still in progress),
+ the update is buffered and will be replayed once it completes.
+
+ Args:
+ relay_id: Composite ID, e.g. "aabbcc112233ddeeff445566-2".
+ state_dict: At minimum {"state": bool}; may include {"brightness": int}.
+ """
+ if self.data is None:
+ # First refresh not yet complete – buffer for replay.
+ self._mqtt_buffer.append((relay_id, state_dict))
+ _LOGGER.debug("Tinxy MQTT: buffered update for %s (coordinator not ready)", relay_id)
+ return
+
+ if relay_id not in self.data:
+ _LOGGER.debug("Tinxy MQTT: received update for unknown relay %s", relay_id)
+ return
+
+ # Build updated data dict (shallow copy so DataUpdateCoordinator detects change)
+ new_data = dict(self.data)
+ new_data[relay_id] = {**self.data[relay_id], **state_dict}
+
+ # async_set_updated_data notifies all subscribed CoordinatorEntity instances
+ self.async_set_updated_data(new_data)
+ _LOGGER.debug("Tinxy MQTT state applied: %s → %s", relay_id, state_dict)
diff --git a/custom_components/tinxy/fan.py b/custom_components/tinxy/fan.py
index cf243a3..05d6388 100644
--- a/custom_components/tinxy/fan.py
+++ b/custom_components/tinxy/fan.py
@@ -71,7 +71,7 @@ async def async_setup_entry(
await coordinator.async_config_entry_first_refresh()
all_devices = apidata.list_fans()
- result = await apidata.get_all_status()
+ result = coordinator.data
fans: List[TinxyFan] = []
for device in all_devices:
@@ -202,7 +202,6 @@ async def async_turn_on(
)
except Exception as exc:
_LOGGER.error("Failed to turn on Tinxy fan '%s': %s", self.device.name, exc)
- await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""
@@ -217,7 +216,6 @@ async def async_turn_off(self, **kwargs: Any) -> None:
_LOGGER.info("Turned off Tinxy fan '%s'.", self.device.name)
except Exception as exc:
_LOGGER.error("Failed to turn off Tinxy fan '%s': %s", self.device.name, exc)
- await self.coordinator.async_request_refresh()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""
@@ -236,7 +234,6 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
)
except Exception as exc:
_LOGGER.error("Failed to set preset mode for Tinxy fan '%s': %s", self.device.name, exc)
- await self.coordinator.async_request_refresh()
def _calculate_percent(self, preset_mode: Optional[str]) -> int:
"""
diff --git a/custom_components/tinxy/light.py b/custom_components/tinxy/light.py
index 63de482..f251a03 100644
--- a/custom_components/tinxy/light.py
+++ b/custom_components/tinxy/light.py
@@ -51,11 +51,11 @@ async def async_setup_entry(
async_add_entities: Callback to add entities.
"""
try:
- apidata, coordinator = hass.data[DOMAIN][entry.entry_id]
+ apidata, coordinator = hass.data[DOMAIN][entry.entry_id][0], hass.data[DOMAIN][entry.entry_id][1]
await coordinator.async_config_entry_first_refresh()
all_devices = apidata.list_lights()
- result = await apidata.get_all_status()
+ result = coordinator.data
status_list = {
device["id"]: {**device, **result[device["id"]]}
for device in all_devices if device["id"] in result
@@ -311,4 +311,3 @@ async def async_turn_off(self, **kwargs: Any) -> None:
)
except Exception as exc:
LOGGER.error("Failed to turn OFF Tinxy light %s: %s", self.idx, exc)
- await self.coordinator.async_request_refresh()
diff --git a/custom_components/tinxy/lock.py b/custom_components/tinxy/lock.py
index 5d14524..2249724 100644
--- a/custom_components/tinxy/lock.py
+++ b/custom_components/tinxy/lock.py
@@ -56,12 +56,12 @@ async def async_setup_entry(
entry: ConfigEntry for this integration.
async_add_entities: Callback to add entities.
"""
- apidata, coordinator = hass.data[DOMAIN][entry.entry_id]
+ apidata, coordinator = hass.data[DOMAIN][entry.entry_id][0], hass.data[DOMAIN][entry.entry_id][1]
await coordinator.async_config_entry_first_refresh()
# Gather lock devices and their status
all_devices: List[Dict[str, Any]] = apidata.list_locks()
- status_result: Dict[str, Dict[str, Any]] = await apidata.get_all_status()
+ status_result: Dict[str, Dict[str, Any]] = coordinator.data
lock_entities: List[TinxyLock] = []
for device in all_devices:
@@ -195,7 +195,6 @@ async def async_unlock(self, **kwargs: Any) -> None:
_LOGGER.info(f"Unlock command sent for lock {self.device.id}")
except Exception as exc:
_LOGGER.error(f"Failed to unlock lock {self.device.id}: {exc}")
- await self.coordinator.async_request_refresh()
async def async_lock(self, **kwargs: Any) -> None:
"""
@@ -210,5 +209,4 @@ async def async_lock(self, **kwargs: Any) -> None:
)
_LOGGER.info(f"Lock command sent for lock {self.device.id}")
except Exception as exc:
- _LOGGER.error(f"Failed to lock lock {self.device.id}: {exc}")
- await self.coordinator.async_request_refresh()
\ No newline at end of file
+ _LOGGER.error(f"Failed to lock lock {self.device.id}: {exc}")
\ No newline at end of file
diff --git a/custom_components/tinxy/manifest.json b/custom_components/tinxy/manifest.json
index bde7baf..e302d3f 100644
--- a/custom_components/tinxy/manifest.json
+++ b/custom_components/tinxy/manifest.json
@@ -5,8 +5,8 @@
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/arevindh/ha-tinxy-cloud",
- "iot_class": "cloud_polling",
+ "iot_class": "cloud_push",
"issue_tracker": "https://github.com/arevindh/ha-tinxy-cloud/issues",
- "requirements": [],
- "version": "4.0.2"
+ "requirements": ["aiomqtt>=1.2.1"],
+ "version": "5.0.0"
}
diff --git a/custom_components/tinxy/mqtt_client.py b/custom_components/tinxy/mqtt_client.py
new file mode 100644
index 0000000..957b3a8
--- /dev/null
+++ b/custom_components/tinxy/mqtt_client.py
@@ -0,0 +1,326 @@
+"""
+Tinxy MQTT Client
+
+Manages persistent MQTT connection to the Tinxy broker for real-time device state
+updates (subscribe) and device control commands (publish).
+
+Protocol notes derived from broker observation:
+ Subscribe : /{device_id}/#
+ Status msg : /{device_id}/info → {"rssi":…,"status":1,"state":"0011","bright":"100100100100"}
+ state[i] → relay (i+1) on/off ('1'=on, '0'=off)
+ bright[i*3:(i+1)*3] → brightness % for relay (i+1), zero-padded to 3 chars
+ Command msg: /{device_id} ← publish {"n": relay_no, "on": "1"|"0"}
+ ← publish {"n": relay_no, "bright": value}
+"""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+from typing import Any, Awaitable, Callable, Dict, Optional, Set
+
+import aiomqtt
+
+from homeassistant.core import HomeAssistant
+from .tinxycloud import TinxyMQTTCredentials
+
+_LOGGER = logging.getLogger(__name__)
+
+RECONNECT_DELAY = 5 # seconds – network drop reconnect delay
+MAX_AUTH_RETRIES = 3 # max consecutive credential-refresh attempts
+
+
+class TinxyMQTTClient:
+ """
+ Lightweight MQTT client for the Tinxy broker.
+
+ Subscribes to every known device topic, parses /info payloads into
+ per-relay state dicts, and forwards them to a caller-supplied callback.
+
+ Also exposes async_publish_command() for sending toggle / brightness commands.
+ """
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ on_state_update: Callable[[str, Dict[str, Any]], None],
+ credentials_fetcher: Callable[[], Awaitable[TinxyMQTTCredentials]],
+ ) -> None:
+ """
+ Initialise the client (does NOT connect yet).
+
+ Args:
+ hass: HomeAssistant instance.
+ on_state_update: Callback fired with (relay_id, state_dict) on every
+ /info message.
+ credentials_fetcher: Async callable that returns fresh TinxyMQTTCredentials.
+ Called once at startup and again whenever the broker
+ rejects the connection (e.g. after a password reset).
+ """
+ self._hass = hass
+ self._on_state_update = on_state_update
+ self._credentials_fetcher = credentials_fetcher
+ self._credentials: Optional[TinxyMQTTCredentials] = None
+ self._device_ids: Set[str] = set()
+ self._task: Optional[asyncio.Task] = None
+ self._client: Optional[aiomqtt.Client] = None
+
+ # ------------------------------------------------------------------
+ # Public API
+ # ------------------------------------------------------------------
+
+ async def async_start(self, device_ids: list[str]) -> None:
+ """Fetch credentials, then start the MQTT background loop."""
+ self._device_ids = set(device_ids)
+ # Fetch credentials eagerly so a bad API key fails at setup time.
+ self._credentials = await self._credentials_fetcher()
+ self._task = self._hass.async_create_background_task(
+ self._run_loop(), "tinxy_mqtt_loop"
+ )
+ _LOGGER.info(
+ "Tinxy MQTT client started for %d devices on %s:%d",
+ len(device_ids), self._credentials.broker, self._credentials.port,
+ )
+
+ async def async_stop(self) -> None:
+ """Cancel the background loop."""
+ if self._task and not self._task.done():
+ self._task.cancel()
+ try:
+ await self._task
+ except asyncio.CancelledError:
+ pass
+ self._client = None
+ _LOGGER.debug("Tinxy MQTT client stopped.")
+
+ async def async_publish_command(
+ self,
+ device_id: str,
+ relay_no: int,
+ state: str,
+ brightness: Optional[int] = None,
+ ) -> None:
+ """
+ Publish a toggle or brightness command to a device.
+
+ Args:
+ device_id: The Tinxy device _id (not the relay composite id).
+ relay_no: 1-based relay number.
+ state: "ON" or "OFF" (used when brightness is None).
+ brightness: 0-100 percentage; when provided, sends a brightness command
+ instead of a simple toggle.
+ """
+ if self._client is None:
+ _LOGGER.warning(
+ "Tinxy MQTT: cannot publish command for %s – not connected.", device_id
+ )
+ return
+
+ if brightness is not None:
+ payload = json.dumps({"n": relay_no, "bright": brightness, "by": "Home Assistant"})
+ else:
+ payload = json.dumps({"n": relay_no, "on": "1" if state == "ON" else "0", "by": "Home Assistant"})
+
+ topic = f"/{device_id}"
+ try:
+ await self._client.publish(topic, payload, qos=0)
+ _LOGGER.debug("MQTT publish → %s : %s", topic, payload)
+ except Exception as exc: # noqa: BLE001
+ _LOGGER.error("Tinxy MQTT: failed to publish command: %s", exc)
+
+ # ------------------------------------------------------------------
+ # Internal
+ # ------------------------------------------------------------------
+
+ async def _run_loop(self) -> None:
+ """Persistent MQTT loop with automatic reconnection and credential refresh."""
+ auth_retries = 0
+
+ while True:
+ try:
+ creds = self._credentials
+ async with aiomqtt.Client(
+ hostname=creds.broker,
+ port=creds.port,
+ username=creds.username,
+ password=creds.password,
+ identifier=f"ha-tinxy-{creds.username}",
+ ) as client:
+ self._client = client
+ auth_retries = 0 # reset on successful connect
+ _LOGGER.info(
+ "Tinxy MQTT connected to %s as %s",
+ creds.broker, creds.username,
+ )
+
+ for device_id in self._device_ids:
+ await client.subscribe(f"/{device_id}/#", qos=0)
+ _LOGGER.debug("Subscribed to /%s/#", device_id)
+
+ async for message in client.messages:
+ self._dispatch_message(str(message.topic), message.payload)
+
+ except aiomqtt.MqttConnectError as exc:
+ # Broker rejected the connection – likely stale credentials
+ # (e.g. user reset their password).
+ self._client = None
+ auth_retries += 1
+ if auth_retries > MAX_AUTH_RETRIES:
+ _LOGGER.error(
+ "Tinxy MQTT: broker rejected credentials %d times – giving up. "
+ "Reload the integration after fixing the account password.",
+ auth_retries,
+ )
+ return
+ _LOGGER.warning(
+ "Tinxy MQTT: broker rejected connection (%s) – refreshing credentials "
+ "(attempt %d/%d).",
+ exc, auth_retries, MAX_AUTH_RETRIES,
+ )
+ try:
+ self._credentials = await self._credentials_fetcher()
+ except Exception as fetch_exc: # noqa: BLE001
+ _LOGGER.error(
+ "Tinxy MQTT: failed to refresh credentials: %s – retrying in %ds",
+ fetch_exc, RECONNECT_DELAY,
+ )
+ await asyncio.sleep(RECONNECT_DELAY)
+ # Reconnect immediately with fresh credentials (no extra sleep).
+
+ except aiomqtt.MqttError as exc:
+ self._client = None
+ _LOGGER.warning(
+ "Tinxy MQTT connection lost: %s – reconnecting in %ds",
+ exc, RECONNECT_DELAY,
+ )
+ await asyncio.sleep(RECONNECT_DELAY)
+ except asyncio.CancelledError:
+ self._client = None
+ _LOGGER.debug("Tinxy MQTT loop cancelled.")
+ return
+ except Exception as exc: # noqa: BLE001
+ self._client = None
+ _LOGGER.error(
+ "Tinxy MQTT unexpected error: %s – reconnecting in %ds",
+ exc, RECONNECT_DELAY,
+ )
+ await asyncio.sleep(RECONNECT_DELAY)
+
+ def _dispatch_message(self, topic: str, payload: bytes) -> None:
+ """Route an incoming MQTT message to the appropriate handler."""
+ try:
+ data = json.loads(payload)
+ except (json.JSONDecodeError, ValueError, TypeError):
+ _LOGGER.debug("Tinxy MQTT: ignoring non-JSON payload on %s", topic)
+ return
+
+ # Topic format: /{device_id} or /{device_id}/subtopic
+ parts = topic.strip("/").split("/")
+ if not parts:
+ return
+
+ device_id = parts[0]
+ subtopic = parts[1] if len(parts) > 1 else None
+
+ try:
+ if subtopic == "info":
+ self._handle_info(device_id, data)
+ elif subtopic is None:
+ # Root topic /{device_id} – command-echo from the broker.
+ # Arrives ~200 ms before the /info confirmation; use it for
+ # an immediate optimistic state update.
+ self._handle_command_echo(device_id, data)
+ except Exception as exc: # noqa: BLE001
+ _LOGGER.warning(
+ "Tinxy MQTT: error handling message on %s: %s", topic, exc
+ )
+
+ def _handle_command_echo(
+ self, device_id: str, data: Dict[str, Any]
+ ) -> None:
+ """
+ Handle a command-echo on /{device_id} (no subtopic).
+
+ Payload shapes observed:
+ {"n": 2, "on": "1"} – relay toggle (on = "1" | "0")
+ {"n": 1, "bright": 66} – brightness change
+
+ Fires an immediate optimistic callback. The authoritative /info
+ message that follows will overwrite it with the confirmed state.
+ """
+ relay_no = data.get("n")
+ if relay_no is None:
+ return
+
+ relay_id = f"{device_id}-{relay_no}"
+ state_dict: Dict[str, Any] = {}
+
+ if "on" in data:
+ state_dict["state"] = str(data["on"]) == "1"
+ if "bright" in data:
+ try:
+ state_dict["brightness"] = int(data["bright"])
+ except (ValueError, TypeError):
+ pass
+ # A brightness command implies the relay is on
+ if "state" not in state_dict:
+ state_dict["state"] = True
+
+ if not state_dict:
+ return
+
+ _LOGGER.debug("MQTT command-echo for %s: %s", relay_id, state_dict)
+ try:
+ self._on_state_update(relay_id, state_dict)
+ except Exception as exc: # noqa: BLE001
+ _LOGGER.error(
+ "Tinxy MQTT: error in command-echo callback for %s: %s", relay_id, exc
+ )
+
+ def _handle_info(self, device_id: str, data: Dict[str, Any]) -> None:
+ """
+ Parse a /{device_id}/info message and fire per-relay callbacks.
+
+ state : string where state[i] == '1' → relay (i+1) is ON
+ bright : string where bright[i*3:(i+1)*3] gives brightness% for relay (i+1)
+ values are zero-padded to 3 chars, e.g. "066100100100"
+ """
+ # Defensively cast to str – some firmware versions send these as numbers
+ state_str = str(data.get("state", ""))
+ bright_str = str(data.get("bright", ""))
+
+ for i, char in enumerate(state_str):
+ relay_no = i + 1
+ relay_id = f"{device_id}-{relay_no}"
+
+ state_dict: Dict[str, Any] = {"state": char == "1"}
+
+ # status field from /info marks the device as online (1) or offline (0)
+ if "status" in data:
+ state_dict["status"] = data["status"]
+
+ # Parse brightness for this relay if available
+ b_start = i * 3
+ b_end = b_start + 3
+ if len(bright_str) >= b_end:
+ try:
+ state_dict["brightness"] = int(bright_str[b_start:b_end])
+ except ValueError:
+ pass
+
+ # Forward device-level diagnostics on every relay update so the
+ # RSSI sensor (keyed on relay -1) can read them from coordinator data.
+ if "rssi" in data:
+ state_dict["rssi"] = data["rssi"]
+ if "ip" in data:
+ state_dict["ip"] = data["ip"]
+ if "version" in data:
+ state_dict["fw_version"] = data["version"]
+
+ try:
+ self._on_state_update(relay_id, state_dict)
+ except Exception as exc: # noqa: BLE001
+ _LOGGER.error(
+ "Tinxy MQTT: error in state-update callback for %s: %s", relay_id, exc
+ )
diff --git a/custom_components/tinxy/sensor.py b/custom_components/tinxy/sensor.py
new file mode 100644
index 0000000..3c41e2c
--- /dev/null
+++ b/custom_components/tinxy/sensor.py
@@ -0,0 +1,151 @@
+"""
+Tinxy Sensor Platform – diagnostic sensors (RSSI, IP address).
+
+Two diagnostic sensors are created per physical Tinxy device:
+ • RSSI – signal strength in dBm
+ • IP – current IP address on the local network
+
+Both values arrive via MQTT /info messages. Devices that never send these
+fields (e.g. wired locks, EVA hub) will show state "unavailable" until a
+message with the relevant field is received.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any, Dict, Optional
+
+from homeassistant.components.sensor import SensorEntity, SensorStateClass
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EntityCategory, SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN
+from .coordinator import TinxyUpdateCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Create RSSI and IP diagnostic sensors per physical Tinxy device."""
+ apidata = hass.data[DOMAIN][entry.entry_id][0]
+ coordinator: TinxyUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][1]
+
+ await coordinator.async_config_entry_first_refresh()
+
+ seen: set[str] = set()
+ entities: list[SensorEntity] = []
+
+ for device in apidata.list_all_devices():
+ device_id: str = device["device_id"]
+ if device_id in seen:
+ continue
+ seen.add(device_id)
+
+ relay_key = f"{device_id}-1"
+ if relay_key not in (coordinator.data or {}):
+ continue
+
+ entities.append(TinxyRSSISensor(coordinator, relay_key))
+ entities.append(TinxyIPSensor(coordinator, relay_key))
+
+ async_add_entities(entities)
+ _LOGGER.info("Tinxy: added %d diagnostic sensor(s).", len(entities))
+
+
+class _TinxyDiagnosticSensor(CoordinatorEntity, SensorEntity):
+ """Shared base for Tinxy diagnostic sensors."""
+
+ _attr_entity_category = EntityCategory.DIAGNOSTIC
+
+ def __init__(self, coordinator: TinxyUpdateCoordinator, relay_key: str) -> None:
+ super().__init__(coordinator)
+ self._relay_key = relay_key
+
+ @property
+ def _data(self) -> Dict[str, Any]:
+ return self.coordinator.data.get(self._relay_key, {})
+
+ @property
+ def _device_name(self) -> str:
+ return (
+ self._data.get("device", {}).get("name")
+ or self._data.get("name", "Tinxy")
+ )
+
+ @property
+ def device_info(self) -> Dict[str, Any]:
+ return self._data.get("device", {})
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ self.async_write_ha_state()
+
+
+class TinxyRSSISensor(_TinxyDiagnosticSensor):
+ """Signal strength (RSSI) diagnostic sensor.
+
+ Shows dBm value from MQTT /info. Firmware version is an extra attribute.
+ Unavailable on devices that never emit rssi (e.g. wired locks).
+ """
+
+ _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+ _attr_state_class = SensorStateClass.MEASUREMENT
+ _attr_icon = "mdi:wifi"
+
+ @property
+ def unique_id(self) -> str:
+ return f"{self._data.get('device_id')}-rssi"
+
+ @property
+ def name(self) -> str:
+ return f"{self._device_name} RSSI"
+
+ @property
+ def native_value(self) -> Optional[int]:
+ return self._data.get("rssi")
+
+ @property
+ def available(self) -> bool:
+ # Unavailable until the first MQTT /info with an rssi field arrives.
+ return "rssi" in self._data
+
+ @property
+ def extra_state_attributes(self) -> Dict[str, Any]:
+ attrs: Dict[str, Any] = {}
+ if "fw_version" in self._data:
+ attrs["firmware_version"] = self._data["fw_version"]
+ return attrs
+
+
+class TinxyIPSensor(_TinxyDiagnosticSensor):
+ """Local IP address diagnostic sensor.
+
+ Shows the device's current LAN IP from MQTT /info.
+ Unavailable on devices that never emit ip (e.g. wired locks).
+ """
+
+ _attr_icon = "mdi:ip-network"
+
+ @property
+ def unique_id(self) -> str:
+ return f"{self._data.get('device_id')}-ip"
+
+ @property
+ def name(self) -> str:
+ return f"{self._device_name} IP Address"
+
+ @property
+ def native_value(self) -> Optional[str]:
+ return self._data.get("ip")
+
+ @property
+ def available(self) -> bool:
+ # Unavailable until the first MQTT /info with an ip field arrives.
+ return "ip" in self._data
diff --git a/custom_components/tinxy/strings.json b/custom_components/tinxy/strings.json
index 44aecc6..afb186c 100644
--- a/custom_components/tinxy/strings.json
+++ b/custom_components/tinxy/strings.json
@@ -2,8 +2,8 @@
"config": {
"step": {
"user": {
- "title": "Tinxy Cloud Setup",
- "description": "Enter your Tinxy Cloud API key to connect your devices.",
+ "title": "Connect to Tinxy",
+ "description": "Enter your Tinxy API key to get started. You can find this in the Tinxy mobile app under Settings.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
diff --git a/custom_components/tinxy/switch.py b/custom_components/tinxy/switch.py
index dc70a93..48084d9 100644
--- a/custom_components/tinxy/switch.py
+++ b/custom_components/tinxy/switch.py
@@ -46,11 +46,11 @@ async def async_setup_entry(
async_add_entities: Callback to add entities to Home Assistant.
"""
try:
- apidata, coordinator = hass.data[DOMAIN][entry.entry_id]
+ apidata, coordinator = hass.data[DOMAIN][entry.entry_id][0], hass.data[DOMAIN][entry.entry_id][1]
await coordinator.async_config_entry_first_refresh()
all_devices = apidata.list_switches()
- result = await apidata.get_all_status()
+ result = coordinator.data
switch_entities: List[TinxySwitch] = []
for device in all_devices:
@@ -160,7 +160,6 @@ async def async_turn_on(self, **kwargs: Any) -> None:
)
except Exception as exc:
_LOGGER.error("Failed to turn ON Tinxy switch %s: %s", self.device.id, exc)
- await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""
@@ -179,4 +178,3 @@ async def async_turn_off(self, **kwargs: Any) -> None:
)
except Exception as exc:
_LOGGER.error("Failed to turn OFF Tinxy switch %s: %s", self.device.id, exc)
- await self.coordinator.async_request_refresh()
diff --git a/custom_components/tinxy/tinxycloud.py b/custom_components/tinxy/tinxycloud.py
index 93f8090..4e59ea7 100644
--- a/custom_components/tinxy/tinxycloud.py
+++ b/custom_components/tinxy/tinxycloud.py
@@ -6,6 +6,8 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
+from .const import MQTT_SERVER, MQTT_PORT
+
class TinxyException(Exception):
"""Base exception for Tinxy API errors."""
@@ -36,6 +38,16 @@ def __post_init__(self) -> None:
raise TinxyException("No API URL was provided.")
+@dataclass
+class TinxyMQTTCredentials:
+ """Per-user MQTT broker credentials fetched from the Tinxy API."""
+
+ username: str # "user-{userId}"
+ password: str # mqttPassword from API
+ broker: str = MQTT_SERVER
+ port: int = MQTT_PORT
+
+
class TinxyCloud:
"""Tinxy Cloud API client for Home Assistant integration."""
@@ -74,6 +86,8 @@ def __init__(self, host_config: TinxyHostConfiguration, web_session: Any) -> Non
self.host_config = host_config
self.web_session = web_session
self.devices: List[Dict[str, Any]] = []
+ # Set by __init__.py after the MQTT client is created
+ self.mqtt_client: Optional[Any] = None
async def tinxy_request(
self, path: str, payload: Optional[Dict[str, Any]] = None, method: str = "GET"
@@ -119,6 +133,41 @@ async def sync_devices(self) -> None:
device_list.extend(self.parse_device(item))
self.devices = device_list
+ async def async_get_mqtt_credentials(self) -> "TinxyMQTTCredentials":
+ """Fetch per-user MQTT credentials from the Tinxy API.
+
+ Endpoint: GET /v2/users/me
+ Response: {"userId": "…", "mqttPassword": "…"}
+
+ MQTT username is constructed as "user-{userId}".
+
+ Returns:
+ TinxyMQTTCredentials with username, password, broker and port.
+
+ Raises:
+ TinxyAuthenticationException: If the API returns an auth error.
+ TinxyException: On any other failure.
+ """
+ try:
+ data = await self.tinxy_request("v2/users/me")
+ except TinxyException as exc:
+ raise TinxyAuthenticationException(
+ f"Failed to fetch MQTT credentials: {exc}"
+ ) from exc
+
+ user_id = data.get("userId")
+ password = data.get("mqttPassword")
+
+ if not user_id or not password:
+ raise TinxyAuthenticationException(
+ f"MQTT credentials response missing fields: {data}"
+ )
+
+ return TinxyMQTTCredentials(
+ username=f"user-{user_id}",
+ password=password,
+ )
+
def list_switches(self) -> List[Dict[str, Any]]:
"""Return a list of all switch devices."""
return [device for device in self.devices if device["device_type"] == "Switch"]
@@ -139,84 +188,6 @@ def list_locks(self) -> List[Dict[str, Any]]:
"""Return a list of all lock devices."""
return [device for device in self.devices if device["gtype"] in self.GTYPE_LOCK]
- async def get_device_state(self, device_id: str, device_number: str) -> Dict[str, Any]:
- """Get the current state of a device.
-
- Args:
- device_id: The device ID
- device_number: The device number
-
- Returns:
- Device state information
- """
- return await self.tinxy_request(
- f"v2/devices/{device_id}/state?deviceNumber={device_number}"
- )
-
- def state_to_val(self, state: str) -> bool:
- """Convert string state to boolean value.
-
- Args:
- state: String state ("ON" or "OFF")
-
- Returns:
- Boolean representation of the state
- """
- return state == "ON"
-
- async def get_all_status(self) -> Dict[str, Dict[str, Any]]:
- """Get status of all devices.
-
- Returns:
- Dictionary mapping device IDs to their status information
- """
- status_data = await self.tinxy_request("v2/devices_state")
- device_status = {}
-
- for status in status_data:
- if "state" not in status:
- continue
-
- if isinstance(status["state"], list):
- # Handle multi-device status
- for item in status["state"]:
- device_id = f"{status['_id']}-{item.get('number', 1)}"
- single_device = self._extract_device_state(item.get("state", {}))
- if "number" not in item:
- single_device["item"] = item
- device_status[device_id] = single_device
- else:
- # Handle single device status
- device_id = f"{status['_id']}-1"
- single_device = self._extract_device_state(status["state"])
- device_status[device_id] = single_device
-
- return device_status
-
- def _extract_device_state(self, state_data: Dict[str, Any]) -> Dict[str, Any]:
- """Extract device state information from API response.
-
- Args:
- state_data: Raw state data from API
-
- Returns:
- Processed device state information
- """
- single_device = {}
-
- if "state" in state_data:
- single_device["state"] = self.state_to_val(state_data["state"])
- if "status" in state_data:
- single_device["status"] = state_data["status"]
- if "brightness" in state_data:
- single_device["brightness"] = state_data["brightness"]
- if "door" in state_data: # For lock devices
- single_device["door"] = state_data["door"]
- if "colorTemperatureInKelvin" in state_data:
- single_device["colorTemperatureInKelvin"] = state_data["colorTemperatureInKelvin"]
-
- return single_device
-
async def set_device_state(
self,
item_id: str,
@@ -224,21 +195,49 @@ async def set_device_state(
state: str,
brightness: Optional[int] = None,
color_temp: Optional[int] = None,
- ) -> Dict[str, Any]:
- """Set device state.
+ ) -> Optional[Dict[str, Any]]:
+ """Set device state via MQTT (preferred) or REST fallback.
+
+ MQTT command format:
+ topic : /{device_id}
+ payload : {"n": relay_no, "on": "1"|"0"} (toggle)
+ {"n": relay_no, "bright": 0-100} (brightness)
+
+ color_temp is not supported over MQTT; when provided the REST API is
+ used regardless of MQTT availability.
Args:
- item_id: Device identifier
- device_number: Device number
- state: Desired state
- brightness: Optional brightness level
- color_temp: Optional color temperature
+ item_id: Tinxy device _id (the raw MongoDB ID).
+ device_number: 1-based relay/node number.
+ state: "ON" or "OFF".
+ brightness: Optional 0-100 brightness percentage.
+ color_temp: Optional colour temperature in Kelvin.
Returns:
- API response
+ None when sent via MQTT; REST response dict when falling back.
"""
- payload = {"request": {"state": state}, "deviceNumber": device_number}
-
+ # Normalise types – platforms pass relay_no as str and state as int/str
+ relay_no_int: int = int(device_number)
+ if isinstance(state, int):
+ state_str = "ON" if state else "OFF"
+ else:
+ state_str = str(state).upper()
+
+ # --- MQTT path ---
+ if self.mqtt_client is not None and color_temp is None:
+ await self.mqtt_client.async_publish_command(
+ device_id=item_id,
+ relay_no=relay_no_int,
+ state=state_str,
+ brightness=brightness,
+ )
+ return None
+
+ # --- REST fallback (color_temp or no MQTT client) ---
+ payload: Dict[str, Any] = {
+ "request": {"state": state_str},
+ "deviceNumber": relay_no_int,
+ }
if brightness is not None:
payload["request"]["brightness"] = brightness
if color_temp is not None:
diff --git a/custom_components/tinxy/translations/en.json b/custom_components/tinxy/translations/en.json
index 64e8625..316e67c 100644
--- a/custom_components/tinxy/translations/en.json
+++ b/custom_components/tinxy/translations/en.json
@@ -1,15 +1,17 @@
{
"config": {
"abort": {
- "already_configured": "Device is already configured"
+ "already_configured": "Tinxy is already configured. You can manage your devices from the integration page."
},
"error": {
- "cannot_connect": "Failed to connect",
- "invalid_auth": "Invalid authentication",
- "unknown": "Unexpected error"
+ "cannot_connect": "Unable to connect to the Tinxy service. Please check your network and try again.",
+ "invalid_auth": "The API key provided is invalid. Please verify it in the Tinxy mobile app.",
+ "unknown": "An unexpected error occurred. Please try again or check the logs for details."
},
"step": {
"user": {
+ "title": "Connect to Tinxy",
+ "description": "Enter your Tinxy API key to get started. You can find this in the Tinxy mobile app under Settings.",
"data": {
"api_key": "API Key"
}
diff --git a/custom_components/tinxy/translations/hi.json b/custom_components/tinxy/translations/hi.json
new file mode 100644
index 0000000..fee7e7a
--- /dev/null
+++ b/custom_components/tinxy/translations/hi.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy पहले से कॉन्फ़िगर किया हुआ है। आप इंटीग्रेशन पेज से अपने डिवाइस प्रबंधित कर सकते हैं।"
+ },
+ "error": {
+ "cannot_connect": "Tinxy सेवा से कनेक्ट नहीं हो सका। कृपया अपना नेटवर्क जांचें और पुनः प्रयास करें।",
+ "invalid_auth": "दिया गया API Key अमान्य है। कृपया Tinxy मोबाइल ऐप में इसे सत्यापित करें।",
+ "unknown": "एक अप्रत्याशित त्रुटि हुई। कृपया पुनः प्रयास करें या लॉग जांचें।"
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy से कनेक्ट करें",
+ "description": "शुरू करने के लिए अपना Tinxy API Key दर्ज करें। आप इसे Tinxy मोबाइल ऐप की सेटिंग्स में पा सकते हैं।",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/kn.json b/custom_components/tinxy/translations/kn.json
new file mode 100644
index 0000000..c7c9f57
--- /dev/null
+++ b/custom_components/tinxy/translations/kn.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy ಈಗಾಗಲೇ ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾಗಿದೆ. ಇಂಟಿಗ್ರೇಶನ್ ಪುಟದಿಂದ ನಿಮ್ಮ ಸಾಧನಗಳನ್ನು ನಿರ್ವಹಿಸಬಹುದು."
+ },
+ "error": {
+ "cannot_connect": "Tinxy ಸೇವೆಗೆ ಸಂಪರ್ಕಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ. ನಿಮ್ಮ ನೆಟ್ವರ್ಕ್ ಪರಿಶೀಲಿಸಿ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+ "invalid_auth": "ನೀಡಿದ API Key ಅಮಾನ್ಯವಾಗಿದೆ. ದಯವಿಟ್ಟು Tinxy ಮೊಬೈಲ್ ಆ್ಯಪ್ನಲ್ಲಿ ಇದನ್ನು ಪರಿಶೀಲಿಸಿ.",
+ "unknown": "ಅನಿರೀಕ್ಷಿತ ದೋಷ ಸಂಭವಿಸಿದೆ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ ಅಥವಾ ಲಾಗ್ಗಳನ್ನು ಪರಿಶೀಲಿಸಿ."
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy ಗೆ ಸಂಪರ್ಕಿಸಿ",
+ "description": "ಪ್ರಾರಂಭಿಸಲು ನಿಮ್ಮ Tinxy API Key ನಮೂದಿಸಿ. Tinxy ಮೊಬೈಲ್ ಆ್ಯಪ್ನ ಸೆಟ್ಟಿಂಗ್ಸ್ ವಿಭಾಗದಲ್ಲಿ ಇದನ್ನು ಕಾಣಬಹುದು.",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/ml.json b/custom_components/tinxy/translations/ml.json
new file mode 100644
index 0000000..0c5e76a
--- /dev/null
+++ b/custom_components/tinxy/translations/ml.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy ഇതിനകം കോൺഫിഗർ ചെയ്തിട്ടുണ്ട്. ഇന്റഗ്രേഷൻ പേജിൽ നിന്ന് നിങ്ങളുടെ ഉപകരണങ്ങൾ നിയന്ത്രിക്കാം."
+ },
+ "error": {
+ "cannot_connect": "Tinxy സേവനത്തിലേക്ക് കണക്റ്റ് ചെയ്യാൻ കഴിഞ്ഞില്ല. നിങ്ങളുടെ നെറ്റ്വർക്ക് പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.",
+ "invalid_auth": "നൽകിയ API Key അസാധുവാണ്. Tinxy മൊബൈൽ ആപ്പിൽ ഇത് പരിശോധിക്കുക.",
+ "unknown": "അപ്രതീക്ഷിതമായ ഒരു പിശക് സംഭവിച്ചു. വീണ്ടും ശ്രമിക്കുക അല്ലെങ്കിൽ ലോഗുകൾ പരിശോധിക്കുക."
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy-യിലേക്ക് കണക്റ്റ് ചെയ്യുക",
+ "description": "ആരംഭിക്കാൻ നിങ്ങളുടെ Tinxy API Key നൽകുക. Tinxy മൊബൈൽ ആപ്പിന്റെ സെറ്റിംഗ്സ് വിഭാഗത്തിൽ ഇത് കണ്ടെത്താം.",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/mr.json b/custom_components/tinxy/translations/mr.json
new file mode 100644
index 0000000..c025df7
--- /dev/null
+++ b/custom_components/tinxy/translations/mr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy आधीच कॉन्फिगर केलेले आहे. इंटिग्रेशन पेजवरून तुम्ही तुमची उपकरणे व्यवस्थापित करू शकता."
+ },
+ "error": {
+ "cannot_connect": "Tinxy सेवेशी कनेक्ट करणे शक्य झाले नाही. कृपया तुमचे नेटवर्क तपासा आणि पुन्हा प्रयत्न करा.",
+ "invalid_auth": "दिलेली API Key अवैध आहे. कृपया Tinxy मोबाइल अॅपमध्ये ती सत्यापित करा.",
+ "unknown": "एक अनपेक्षित त्रुटी आली. कृपया पुन्हा प्रयत्न करा किंवा लॉग तपासा."
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy शी कनेक्ट करा",
+ "description": "सुरू करण्यासाठी तुमची Tinxy API Key प्रविष्ट करा. ही Tinxy मोबाइल अॅपच्या सेटिंग्ज विभागात सापडेल.",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/pa.json b/custom_components/tinxy/translations/pa.json
new file mode 100644
index 0000000..847e512
--- /dev/null
+++ b/custom_components/tinxy/translations/pa.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy ਪਹਿਲਾਂ ਹੀ ਕੌਂਫਿਗਰ ਕੀਤਾ ਜਾ ਚੁੱਕਾ ਹੈ। ਤੁਸੀਂ ਇੰਟੀਗ੍ਰੇਸ਼ਨ ਪੇਜ ਤੋਂ ਆਪਣੇ ਡਿਵਾਈਸਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰ ਸਕਦੇ ਹੋ।"
+ },
+ "error": {
+ "cannot_connect": "Tinxy ਸੇਵਾ ਨਾਲ ਕਨੈਕਟ ਨਹੀਂ ਹੋ ਸਕਿਆ। ਕਿਰਪਾ ਕਰਕੇ ਆਪਣਾ ਨੈੱਟਵਰਕ ਜਾਂਚੋ ਅਤੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
+ "invalid_auth": "ਦਿੱਤਾ ਗਿਆ API Key ਗਲਤ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ Tinxy ਮੋਬਾਈਲ ਐਪ ਵਿੱਚ ਇਸਦੀ ਪੁਸ਼ਟੀ ਕਰੋ।",
+ "unknown": "ਇੱਕ ਅਣਕਿਆਸੀ ਗਲਤੀ ਆਈ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜਾਂ ਲੌਗ ਜਾਂਚੋ।"
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy ਨਾਲ ਕਨੈਕਟ ਕਰੋ",
+ "description": "ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਆਪਣੀ Tinxy API Key ਦਰਜ ਕਰੋ। ਇਹ Tinxy ਮੋਬਾਈਲ ਐਪ ਦੀਆਂ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਮਿਲੇਗੀ।",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/ta.json b/custom_components/tinxy/translations/ta.json
new file mode 100644
index 0000000..99cef69
--- /dev/null
+++ b/custom_components/tinxy/translations/ta.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy ஏற்கனவே உள்ளமைக்கப்பட்டுள்ளது. இன்டெக்ரேஷன் பக்கத்தில் இருந்து உங்கள் சாதனங்களை நிர்வகிக்கலாம்."
+ },
+ "error": {
+ "cannot_connect": "Tinxy சேவையுடன் இணைக்க முடியவில்லை. உங்கள் நெட்வொர்க்கை சரிபார்த்து மீண்டும் முயற்சிக்கவும்.",
+ "invalid_auth": "வழங்கிய API Key தவறானது. Tinxy மொபைல் ஆப்பில் இதை சரிபார்க்கவும்.",
+ "unknown": "எதிர்பாராத பிழை ஏற்பட்டது. மீண்டும் முயற்சிக்கவும் அல்லது பதிவுகளை சரிபார்க்கவும்."
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy-உடன் இணைக்கவும்",
+ "description": "தொடங்க உங்கள் Tinxy API Key ஐ உள்ளிடவும். Tinxy மொபைல் ஆப்பின் அமைப்புகள் பகுதியில் இதை காணலாம்.",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
diff --git a/custom_components/tinxy/translations/te.json b/custom_components/tinxy/translations/te.json
new file mode 100644
index 0000000..0b58b75
--- /dev/null
+++ b/custom_components/tinxy/translations/te.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tinxy ఇప్పటికే కాన్ఫిగర్ చేయబడింది. ఇంటిగ్రేషన్ పేజీ నుండి మీ పరికరాలను నిర్వహించవచ్చు."
+ },
+ "error": {
+ "cannot_connect": "Tinxy సేవకు కనెక్ట్ చేయడం సాధ్యం కాలేదు. మీ నెట్వర్క్ను తనిఖీ చేసి మళ్ళీ ప్రయత్నించండి.",
+ "invalid_auth": "అందించిన API Key చెల్లదు. దయచేసి Tinxy మొబైల్ యాప్లో దీన్ని ధృవీకరించండి.",
+ "unknown": "అనుకోని లోపం సంభవించింది. దయచేసి మళ్ళీ ప్రయత్నించండి లేదా లాగ్లు తనిఖీ చేయండి."
+ },
+ "step": {
+ "user": {
+ "title": "Tinxy కి కనెక్ట్ చేయండి",
+ "description": "ప్రారంభించడానికి మీ Tinxy API Key ని నమోదు చేయండి. Tinxy మొబైల్ యాప్ సెట్టింగ్స్ విభాగంలో ఇది కనుగొనవచ్చు.",
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}