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