[
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Jiaxin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Xgimi-4-Home-Assistant\n<img src=\"https://brands.home-assistant.io/xgimi/logo.png\"  width=\"360\" height=\"120\">  \n\nXGIMI integration for home assistant.  \nPlease give me a star :star_struck: if you like it.  \n\n\n## 📦Install\n### Manually\n1. Download the latest source code ``xgimi.zip`` from [Releases](https://github.com/manymuch/Xgimi-4-Home-Assistant/releases)  \n2. Unzip and copy the folder `xgimi` into home assistant `custom_components`  \n2. Restart home assistant  \n### Via HACS\n1. Install [HACS](https://hacs.xyz/) if you do not have that already\n2. In the Home Assitant HACS Tab, click on the three dots at the top right\n3. Choose `Custom repositories`\n4. Paste `https://github.com/manymuch/Xgimi-4-Home-Assistant/` to the repository field, choose the `Integration` category and click `ADD`\n5. Close the dialog\n6. Disable the repository filter (hit `CLEAR` on the right of the `Filtering by Downloaded` text)\n7. Type `Xgimi` to the Search bar and select `Xgimi Projector Remote`\n8. Click `DOWNLOAD` on the bottom right\n9. Click `DOWNLOAD` in the dialog that just appeared\n10. Restart home assistant\n\n## 🛜Get BLE token\n\nThe integration communicates with xgimi projector by UDP using local IP except for the **poweron** command. Once the projector is powered off, the only way to turn up is sending a special ble advertisement. Such a ble advertisement contains a special token called `manufacture data`. The `manufacture data` is **different for each device even for same model**.  \n\n\nThis token can be obtained using Home Assistant's built-in **Bluetooth Advertisement Monitor**.\n\n### 1. Open the Bluetooth Advertisement Monitor\n\nYou can access the monitor directly from your Home Assistant instance:\n\n👉 [Open Bluetooth Advertisement Monitor](https://my.home-assistant.io/redirect/bluetooth_advertisement_monitor/)\n\nAlternatively, navigate manually:\n\n1. Go to **Settings → Devices & Services → Bluetooth → Configure**.\n2. Select **Advertisement Monitor**.\n\nThis view displays all Bluetooth Low Energy (BLE) advertisement packets detected by your Bluetooth adapter.\n\n### 2. Identify the Xgimi Remote MAC Address\n\nBecause the BLE environment may contain dozens of nearby devices, it’s helpful to filter results by MAC address.\n\nIf you don’t know the remote’s MAC address:\n\n1. Pair your Xgimi remote temporarily with an Android phone.\n\n   1. To pair the remote, turn off the projector completely or go to another room and simultaneously press and hold the **Back** and **Home** buttons until the indicator light flashes.\n   2. Look for a device with the name **“XGIMI RC”** or **“BLuetooth 4.0 RC”** and pair with it.\n2. In the Bluetooth settings, open **Device details** to find the **MAC address**.\n3. Note down the first three bytes (the prefix). For example, in many cases XGIMI remotes begin with `1C:`.\n4. After that, pair the remote with the projector again.\n\nYou can later use this prefix to narrow down advertisements in the HA monitor.\n\n### 3. Filter and Locate the Manufacturer Data\n\n1. In the **Advertisement Monitor**, apply a **MAC prefix filter** (e.g., `1C:`) to reduce noise.\n2. Turn off your projector so that the remote control does not automatically connect to it.\n3. Press the **Power** button on the remote to send a special BLE advertisement frame.\n4. Look for entries where the **Manufacturer data** field is populated and has an entry with ID: `70`. This data is a hexadecimal payload broadcast by the remote and contains the BLE token required by the integration.\n\nExample frame data:\n\n```json\n{\n\"name\": \"BLuetooth 4.0 RC\",\n\"address\": \"1C:XX:XX:XX:XX:XX\",\n\"rssi\": -66,\n\"manufacturer_data\": {\n    \"13\": \"383800000001\",\n    \"70\": \"51f55a6d78e450ffffff0000000b000d\"\n},\n\"service_data\": {},\n\"service_uuids\": [\n    \"00001812-0000-1000-8000-00805f9b34fb\"\n],\n\"source\": \"30:24:XX:XX:XX:XX\",\n\"connectable\": true,\n\"time\": 1761087659.0729098,\n\"tx_power\": null,\n\"raw\": \"0000000000000c0000000f0f000000000e0000000000000000000d000000\"\n}\n```\n\nCopy the full value from the manufacturer data with ID `70` — this is the token required for integration configuration. In this example:\n``51f55a6d78e450ffffff0000000b000d``.\n\n\n## 🏗️Setup\n\n1. Prepare the following:  \n    * ``host``: local IP of the projector, check the router or the setting in the projector's menu.  (For all commands via LAN: poweroff, volume, etc.)  \n    * ``token``: BLE token to power on the projector.  (For poweron command only, via bluetooth signals)\n\n2. Make sure your projector is **powered on** and can be reached via LAN by home assistant.\n3. Add new integration, search for xgimi\n4. Enter your projector information, for example:\n    ```bash\n    name: z6x\n    host: 192.168.0.115\n    token: 51F55A6D78E450FFFFFF0000000B000D\n    ```\n\n## 📺How to use\nThe integration setup up a remote entity: e.g. `remote.z6x`.  \nExample usage of remote.send_command service:  \n```yaml\naction: remote.send_command\ndata:\n    command: volumeup\ntarget:\n    entity_id: remote.z6x\n```\nAvailable commands:  \nThe below commands work for all models:  \n```\nplay, pause, power, back, home, menu, right, left,\nup, down, volumedown, volumeup,\npoweron, poweroff, volumemute\n```\nThe below commands may only work for some models, you can have a try and good luck :-)  \n```\nautofocus, autofocus_new,\nmanual_focus_left, manual_focus_right,\nmotor_left_overstep, motor_left_start,\nmotor_right_overstep, motor_right_start, motor_stop,\nshortcut_setting, choose_source, hibernate, xmusic\n```\n\n### Dashboard example\nSee [tv-card-example.yaml](assets/tv-card-example.yaml) for a dashboard example using [tv-card](https://github.com/marrobHD/tv-card)  \n<img src=\"assets/tv_card.png\"  width=\"200\" height=\"220\"> \n\n\n### Troubleshoot\n\n1. If you are running Home Assistant with docker, make sure HA is accessible to the bluetooth, see [issue #12](https://github.com/manymuch/Xgimi-4-Home-Assistant/issues/12).  \n2. Make sure the bluetooth signal from HA host machine can reach the projector without blockage.\n3. If you can not use LAN control (volumeup, poweroff, etc..), it is likely that your XGIMI is running on native Android TV OS. Try to use [Android TV Remote](https://www.home-assistant.io/integrations/androidtv_remote/). See also [issue #40](https://github.com/manymuch/Xgimi-4-Home-Assistant/issues/40). \n\n### More Related threads about BLE token\n\n* [issue #5](https://github.com/manymuch/Xgimi-4-Home-Assistant/issues/5)\n* [issue #31](https://github.com/manymuch/Xgimi-4-Home-Assistant/issues/31)\n* [issue #54](https://github.com/manymuch/Xgimi-4-Home-Assistant/issues/54) Thanks @mik-laj for contributing usage of advertisement monitor in HA. \n* [stackoverflow discussion](https://stackoverflow.com/questions/69921353/how-can-i-clone-a-non-paired-ble-signal-from-a-remote-to-trigger-a-device/75551013#75551013)\n\n## TODO\n- auto discovery  \n- media player entity   \n\n\nContributions and suggestions are welcome!  \nPlease give me a star :star_struck: if you like it.\n"
  },
  {
    "path": "assets/tv-card-example.yaml",
    "content": "type: custom:tv-card\nentity: remote.z6x\ntv: true\nleft:\n  service: remote.send_command\n  service_data:\n    command: left\n    entity_id: remote.z6x\nright:\n  service: remote.send_command\n  service_data:\n    command: right\n    entity_id: remote.z6x\nup:\n  service: remote.send_command\n  service_data:\n    command: up\n    entity_id: remote.z6x\ndown:\n  service: remote.send_command\n  service_data:\n    command: down\n    entity_id: remote.z6x\nselect:\n  service: remote.send_command\n  service_data:\n    command: play\n    entity_id: remote.z6x\nback:\n  service: remote.send_command\n  service_data:\n    command: back\n    entity_id: remote.z6x\nvolume_up:\n  service: remote.send_command\n  service_data:\n    command: volumeup\n    entity_id: remote.z6x\nvolume_down:\n  service: remote.send_command\n  service_data:\n    command: volumedown\n    entity_id: remote.z6x\nvolume_mute:\n  service: remote.send_command\n  service_data:\n    command: volumemute\n    entity_id: remote.z6x\npower:\n  service: remote.send_command\n  service_data:\n    command: power\n    entity_id: remote.z6x\nhome:\n  service: remote.send_command\n  service_data:\n    command: home\n    entity_id: remote.z6x\n\n"
  },
  {
    "path": "custom_components/xgimi/__init__.py",
    "content": "\"\"\"Xgimi Projector Integration\"\"\"\nfrom __future__ import annotations\n\nfrom typing import Final\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, Platform\nfrom homeassistant.core import HomeAssistant\n\nfrom .const import DOMAIN\n\nPLATFORMS: Final[list[Platform]] = [\n    Platform.REMOTE,\n]\n\nasync def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:\n    \"\"\"Set up a config entry.\"\"\"\n    hass.data.setdefault(DOMAIN, {})\n    config = {}\n    for k in [CONF_HOST, CONF_TOKEN, CONF_NAME]:\n        config[k] = config_entry.data.get(k)\n\n    hass.data[DOMAIN][config_entry.entry_id] = config\n\n    await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)\n\n    return True\n\nasync def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:\n    \"\"\"Unload a config entry.\"\"\"\n    unload_ok = await hass.config_entries.async_unload_platforms(\n        config_entry, PLATFORMS\n    )\n\n    if unload_ok:\n        hass.data[DOMAIN].pop(config_entry.entry_id)\n\n    return unload_ok\n"
  },
  {
    "path": "custom_components/xgimi/config_flow.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\nimport voluptuous as vol\n\nfrom homeassistant import config_entries\nfrom homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN\nfrom homeassistant.data_entry_flow import FlowResult\nfrom homeassistant.util.network import is_host_valid\n\nfrom .const import (\n    DOMAIN,\n)\n\n\nclass XgimiConfigFLow(config_entries.ConfigFlow, domain=DOMAIN):\n    VERSION = 1\n\n    async def async_step_user(\n        self, user_input: dict[str, Any] | None = None\n    ) -> FlowResult:\n        errors: dict[str, str] = {}\n\n        if user_input is not None:\n            host = user_input[CONF_HOST]\n            name = user_input[CONF_NAME]\n            token = user_input[CONF_TOKEN]\n            if not is_host_valid(host):\n                errors[CONF_HOST] = \"invalid_host\"\n            else:\n                await self.async_set_unique_id(f\"{name}-{token}\")\n                self._abort_if_unique_id_configured()\n                return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)\n        else:\n            user_input = {}\n\n        return self.async_show_form(\n            step_id=\"user\",\n            data_schema=vol.Schema({\n                vol.Required(CONF_NAME, default=user_input.get(CONF_NAME, vol.UNDEFINED)): str,\n                vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, vol.UNDEFINED)): str,\n                vol.Required(CONF_TOKEN, default=user_input.get(CONF_TOKEN, vol.UNDEFINED)): str,\n            }),\n            errors=errors,\n        )\n"
  },
  {
    "path": "custom_components/xgimi/const.py",
    "content": "\"\"\"Constants for Xgimi Integration.\"\"\"\n# Base component constants\nNAME = \"Xgimi Projector Integration\"\nDOMAIN = \"xgimi\"\nDOMAIN_DATA = f\"{DOMAIN}_data\"\nVERSION = \"0.0.8\"\n"
  },
  {
    "path": "custom_components/xgimi/manifest.json",
    "content": "{\n  \"domain\": \"xgimi\",\n  \"name\": \"Xgimi Projector Remote\",\n  \"version\": \"v0.0.8\",\n  \"config_flow\": true,\n  \"integration_type\": \"device\",\n  \"documentation\": \"https://github.com/manymuch/Xgimi-4-Home-Assistant\",\n  \"issue_tracker\": \"https://github.com/manymuch/Xgimi-4-Home-Assistant/issues\",\n  \"requirements\": [\"asyncudp\", \"bluez-peripheral\"],\n  \"codeowners\": [\n    \"@manymuch\"\n  ],\n  \"iot_class\": \"local_polling\"\n}\n"
  },
  {
    "path": "custom_components/xgimi/pyxgimi.py",
    "content": "import asyncudp\nimport asyncio\nfrom bluez_peripheral.util import get_message_bus\nfrom bluez_peripheral.advert import Advertisement\nfrom time import time\n\n\nclass XgimiApi:\n    def __init__(self, ip, command_port, advance_port, alive_port, manufacturer_data) -> None:\n        self.ip = ip\n        self.command_port = command_port  # 16735\n        self.advance_port = advance_port  # 16750\n        self.alive_port = alive_port  # 554\n        self.manufacturer_data = manufacturer_data\n        self._is_on = False\n        self.last_on = time()\n        self.last_off = time()\n\n        self._command_dict = {\n            \"ok\": \"KEYPRESSES:49\",\n            \"play\": \"KEYPRESSES:49\",\n            \"pause\": \"KEYPRESSES:49\",\n            \"power\": \"KEYPRESSES:116\",\n            \"back\": \"KEYPRESSES:48\",\n            \"home\": \"KEYPRESSES:35\",\n            \"menu\": \"KEYPRESSES:139\",\n            \"right\": \"KEYPRESSES:37\",\n            \"left\": \"KEYPRESSES:50\",\n            \"up\": \"KEYPRESSES:36\",\n            \"down\": \"KEYPRESSES:38\",\n            \"volumedown\": \"KEYPRESSES:114\",\n            \"volumeup\": \"KEYPRESSES:115\",\n            \"poweroff\": \"KEYPRESSES:30\",\n            \"volumemute\": \"KEYPRESSES:113\",\n            \"autofocus\": \"KEYPRESSES:2099\",\n            \"autofocus_new\": \"KEYPRESSES:2103\",\n            \"manual_focus_left\": \"KEYPRESSES:2097\",\n            \"manual_focus_right\": \"KEYPRESSES:2098\",\n            \"motor_left_overstep\": \"KEYPRESSES:2095\",\n            \"motor_left_start\": \"KEYPRESSES:2092\",\n            \"motor_right_overstep\": \"KEYPRESSES:2096\",\n            \"motor_right_start\": \"KEYPRESSES:2093\",\n            \"motor_stop\": \"KEYPRESSES:2101\",\n            \"shortcut_setting\": \"KEYPRESSES:2094\",\n            \"choose_source\": \"KEYPRESSES:2102\",\n            \"hibernate\": \"KEYPRESSES:2106\",\n            \"xmusic\": \"KEYPRESSES:2108\",\n        }\n        self._advance_command = str({\"action\": 20000, \"controlCmd\": {\"data\": \"command_holder\",\n                                    \"delayTime\": 0, \"mode\": 5, \"time\": 0, \"type\": 0}, \"msgid\": \"2\"})\n\n    @property\n    def is_on(self) -> bool:\n        \"\"\"Return true if the device is on.\"\"\"\n        return self._is_on\n\n    async def async_fetch_data(self):\n        if time() - self.last_on < 30:\n            self._is_on = True\n        elif time() - self.last_off < 30:\n            self._is_on = False\n        else:\n            alive = await self.async_check_alive()\n            self._is_on = alive\n\n    async def async_check_alive(self):\n        try:\n            _, writer = await asyncio.open_connection(\n                self.ip, self.alive_port)\n            writer.close()\n            await writer.wait_closed()\n            return True\n        except ConnectionRefusedError:\n            return False\n        except Exception:\n            return False\n\n    async def async_ble_power_on(self, manufacturer_data: str, company_id: int = 0x0046, service_uuid: str = \"1812\"):\n        bus = await get_message_bus()\n        advert = Advertisement(\n            localName=\"Bluetooth 4.0 RC\",\n            serviceUUIDs=[service_uuid],\n            manufacturerData={company_id: bytes.fromhex(manufacturer_data)},\n            timeout=1,\n            duration=1000,\n            appearance=961,\n        )\n        await advert.register(bus)\n\n    async def async_robust_ble_power_on(self, manufacturer_data: str, company_id: int = 0x0046, service_uuid: str = \"1812\"):\n        for i in range(10):\n            await self.async_ble_power_on(manufacturer_data, company_id, service_uuid)\n            await asyncio.sleep(1)\n\n    async def async_send_command(self, command) -> None:\n        \"\"\"Send a command to a device.\"\"\"\n        if command in self._command_dict:\n            if command == \"poweroff\":\n                self._is_on = False\n                self.last_off = time()\n            msg = self._command_dict[command]\n            remote_addr = (self.ip, self.command_port)\n            sock = await asyncudp.create_socket(remote_addr=remote_addr)\n            sock.sendto(msg.encode(\"utf-8\"))\n            sock.close()\n        elif command == \"poweron\":\n            self._is_on = True\n            self.last_on = time()\n            await self.async_robust_ble_power_on(self.manufacturer_data)\n        else:\n            msg = self._advance_command.replace(\"command_holder\", command)\n            remote_addr = (self.ip, self.advance_port)\n            sock = await asyncudp.create_socket(remote_addr=remote_addr)\n            sock.sendto(msg.encode(\"utf-8\"))\n            sock.close()\n"
  },
  {
    "path": "custom_components/xgimi/remote.py",
    "content": "\"\"\"Support for the Xgimi Projector.\"\"\"\n\nfrom collections.abc import Iterable\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom .pyxgimi import XgimiApi\n\n\nfrom homeassistant.components.remote import (\n    RemoteEntity,\n)\n\nfrom .const import DOMAIN\n\n\nasync def async_setup_platform(hass, config, async_add_entities, discovery_info=None):\n    \"\"\"Set up the Xiaomi TV platform.\"\"\"\n\n    # If a hostname is set. Discovery is skipped.\n    host = config.get(CONF_HOST)\n    name = config.get(CONF_NAME)\n    token = config.get(CONF_TOKEN)\n    unique_id = f\"{name}-{token}\"\n\n    xgimi_api = XgimiApi(ip=host, command_port=16735, advance_port=16750, alive_port=554,\n                         manufacturer_data=token)\n    async_add_entities([XgimiRemote(xgimi_api, name, unique_id)])\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    config = hass.data[DOMAIN][config_entry.entry_id]\n    host = config[CONF_HOST]\n    name = config[CONF_NAME]\n    token = config[CONF_TOKEN]\n\n    unique_id = config_entry.unique_id\n    assert unique_id is not None\n\n    xgimi_api = XgimiApi(ip=host, command_port=16735, advance_port=16750, alive_port=554,\n                         manufacturer_data=token)\n    async_add_entities([XgimiRemote(xgimi_api, name, unique_id)])\n\n\nclass XgimiRemote(RemoteEntity):\n    \"\"\"An entity for Xgimi Projector\n    \"\"\"\n\n    def __init__(self, xgimi_api, name, unique_id):\n        self.xgimi_api = xgimi_api\n        self._name = name\n        self._icon = \"mdi:projector\"\n        self._unique_id = unique_id\n\n    async def async_update(self):\n        \"\"\"Retrieve latest state.\"\"\"\n        await self.xgimi_api.async_fetch_data()\n\n    @property\n    def is_on(self):\n        \"\"\"Return true if remote is on.\"\"\"\n        return self.xgimi_api._is_on\n\n    @property\n    def name(self):\n        \"\"\"Return the name of the device if any.\"\"\"\n        return self._name\n\n    @property\n    def icon(self):\n        \"\"\"Return the icon to use for device if any.\"\"\"\n        return self._icon\n\n    @property\n    def unique_id(self):\n        \"\"\"Return an unique ID.\"\"\"\n        return self._unique_id\n\n    async def async_turn_on(self, **kwargs):\n        \"\"\"Turn the Xgimi Projector On.\"\"\"\n        # Do the turning on.\n        await self.xgimi_api.async_send_command(\"poweron\")\n\n    async def async_turn_off(self, **kwargs):\n        \"\"\"Turn the Xgimi Projector Off.\"\"\"\n        # Do the turning off.\n        await self.xgimi_api.async_send_command(\"poweroff\")\n\n    async def async_send_command(self, command: Iterable[str], **kwargs) -> None:\n        \"\"\"Send a command to one of the devices.\"\"\"\n        for single_command in command:\n            await self.xgimi_api.async_send_command(single_command)"
  },
  {
    "path": "custom_components/xgimi/translations/en.json",
    "content": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Configure TV info\",\n        \"description\": \"Please make sure your TV turned on before trying to set it up.\",\n        \"data\": {\n          \"name\": \"TV Name\",\n          \"host\": \"TV Host\",\n          \"token\": \"BLE Token\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_host\": \"Invalid hostname or IP address\",\n      \"cannot_connect\": \"Failed to connect\"\n    },\n    \"abort\": {\n      \"already_configured\": \"Device is already configured\"\n    }\n  }\n}"
  },
  {
    "path": "custom_components/xgimi/translations/es.json",
    "content": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Configura la información de la TV\",\n        \"description\": \"Por favor, has de estar seguro que tu TV está encendida antes de intentar configurarla.\",\n        \"data\": {\n          \"name\": \"Nombre de la TV\",\n          \"host\": \"Nombre del host/IP de la TV\",\n          \"token\": \"Token de BLE\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_host\": \"Nombre del host o dirección IP inválidos\",\n      \"cannot_connect\": \"Ha fallado la conexión\"\n    },\n    \"abort\": {\n      \"already_configured\": \"El dispositivo está ya configurado\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/xgimi/translations/fr.json",
    "content": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Configuration du projecteur XGIMI\",\n        \"description\": \"Assurez-vous que le projecteur XGIMI est allumé avant de démarrer la configuration.\",\n        \"data\": {\n          \"name\": \"Nom du projecteur\",\n          \"host\": \"Adresse IP du projecteur\",\n          \"token\": \"Token BLE\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_host\": \"Nom ou adresse IP invalide\",\n      \"cannot_connect\": \"Connexion échouée\"\n    },\n    \"abort\": {\n      \"already_configured\": \"Équipement déjà configuré\"\n    }\n  }\n}\n"
  },
  {
    "path": "custom_components/xgimi/translations/zh-Hans.json",
    "content": "{\n  \"config\": {\n    \"step\": {\n      \"user\": {\n        \"title\": \"Xgimi Projector Remote\",\n        \"description\": \"在配置之前，请确保电视已经打开。\",\n        \"data\": {\n          \"name\": \"电视名称\",\n          \"host\": \"电视 IP\",\n          \"token\": \"蓝牙 Token\"\n        }\n      }\n    },\n    \"error\": {\n      \"invalid_host\": \"IP错误\",\n      \"cannot_connect\": \"连接设备失败\"\n    },\n    \"abort\": {\n      \"already_configured\": \"该设备已经配置过\"\n    }\n  }\n}"
  },
  {
    "path": "hacs.json",
    "content": "{\n    \"name\": \"Xgimi Projector Remote\",\n    \"render_readme\": true,\n    \"hide_default_branch\": true\n}\n"
  }
]