Refactor Kobra X integration into kobrax_lan component

All entites working

- Moved all existing functionality from custom_components/kobrax to custom_components/kobrax_lan.
- Updated manifest, const, and configuration flow to reflect the new component structure.
- Reimplemented API client, coordinator, entity, and platform files under the new component.
- Added support for image entities and binary sensors.
- Ensured compatibility with Home Assistant's latest standards and practices.
This commit is contained in:
Gangoke
2026-05-16 23:32:07 -10:00
parent 9f4a94da24
commit 279995ba13
18 changed files with 88 additions and 22 deletions

View File

@@ -1,12 +0,0 @@
{
"domain": "kobrax",
"name": "Kobra X",
"codeowners": [
"@Gangoke"
],
"config_flow": true,
"documentation": "https://gitea.it-drui.de/viewit/KX-Bridge-Release",
"iot_class": "local_polling",
"issue_tracker": "https://gitea.it-drui.de/viewit/KX-Bridge-Release/issues",
"version": "0.1.0"
}

View File

@@ -39,6 +39,13 @@ class KobraXApiClient:
async def async_get_state(self) -> dict[str, Any]:
return await self._get_json("/api/state")
async def async_get_files(self) -> list[dict[str, Any]]:
data = await self._get_json("/kx/files")
result = data.get("result", [])
if isinstance(result, list):
return result
raise KobraXApiError("Unexpected response for /kx/files")
async def async_pause_print(self) -> None:
await self._post_json("/printer/print/pause", {})

View File

@@ -35,7 +35,6 @@ BINARY_SENSORS: tuple[KobraXBinaryDescription, ...] = (
name="Light State",
value_key="light_on",
icon="mdi:lightbulb",
entity_registry_enabled_default=False,
),
)
@@ -43,6 +42,10 @@ BINARY_SENSORS: tuple[KobraXBinaryDescription, ...] = (
class KobraXBinarySensor(KobraXEntity, BinarySensorEntity):
entity_description: KobraXBinaryDescription
def __init__(self, coordinator, entry, description: KobraXBinaryDescription) -> None:
super().__init__(coordinator, entry, description.key, description.name)
self.entity_description = description
@property
def is_on(self) -> bool:
if self.entity_description.value_key == "kobra_state":
@@ -56,7 +59,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
async_add_entities(
[
KobraXBinarySensor(coordinator, entry, description.key, description.name)
KobraXBinarySensor(coordinator, entry, description)
for description in BINARY_SENSORS
]
)

View File

@@ -54,6 +54,10 @@ BUTTONS: tuple[KobraXButtonDescription, ...] = (
class KobraXActionButton(KobraXEntity, ButtonEntity):
entity_description: KobraXButtonDescription
def __init__(self, coordinator, entry, description: KobraXButtonDescription) -> None:
super().__init__(coordinator, entry, description.key, description.name)
self.entity_description = description
async def async_press(self) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
@@ -76,7 +80,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
async_add_entities(
[
KobraXActionButton(coordinator, entry, description.key, description.name)
KobraXActionButton(coordinator, entry, description)
for description in BUTTONS
]
)

View File

@@ -1,7 +1,7 @@
from datetime import timedelta
NAME = "Kobra X"
DOMAIN = "kobrax"
DOMAIN = "kobrax_lan"
MANUFACTURER = "Anycubic"
CONF_HOST = "host"
@@ -19,4 +19,5 @@ PLATFORMS = [
"select",
"button",
"camera",
"image",
]

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import base64
import binascii
from homeassistant.components.image import ImageEntity
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .entity import KobraXEntity
class KobraXGCodeImage(KobraXEntity, ImageEntity):
def __init__(self, coordinator, entry, key: str, name: str) -> None:
KobraXEntity.__init__(self, coordinator, entry, key, name)
ImageEntity.__init__(self, coordinator.hass)
self._attr_content_type = "image/png"
async def async_image(self) -> bytes | None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
files = await api.async_get_files()
if not files:
return None
active_filename = self.state_data.get("filename")
selected = None
if active_filename:
selected = next((f for f in files if f.get("filename") == active_filename), None)
if selected is None:
selected = files[0]
thumb_b64 = selected.get("thumbnail_b64")
if not thumb_b64:
return None
try:
image = base64.b64decode(thumb_b64, validate=True)
except (ValueError, binascii.Error):
return None
self._attr_image_last_updated = dt_util.utcnow()
return image
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
async_add_entities([KobraXGCodeImage(coordinator, entry, "gcode_thumbnail", "GCode Thumbnail")])

View File

@@ -0,0 +1,12 @@
{
"domain": "kobrax_lan",
"name": "Kobra X LAN",
"codeowners": [
"@Gangoke"
],
"config_flow": true,
"documentation": "https://github.com/gangoke/kobrax-lan-hass-component",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/gangoke/kobrax-lan-hass-component/issues",
"version": "0.1.0"
}

View File

@@ -41,6 +41,7 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="nozzle_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_hotend_temp",
@@ -48,7 +49,7 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="nozzle_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="bed_temp",
@@ -56,6 +57,7 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="bed_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_bed_temp",
@@ -63,7 +65,7 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="bed_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="filename",
@@ -82,7 +84,6 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
name="Total Layers",
value_key="total_layers",
icon="mdi:layers",
entity_registry_enabled_default=False,
),
KobraXSensorDescription(
key="remaining_time",
@@ -95,7 +96,6 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
name="Print Duration",
value_key="print_duration",
icon="mdi:timer-outline",
entity_registry_enabled_default=False,
),
)
@@ -103,6 +103,10 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
class KobraXSensor(KobraXEntity, SensorEntity):
entity_description: KobraXSensorDescription
def __init__(self, coordinator, entry, description: KobraXSensorDescription) -> None:
super().__init__(coordinator, entry, description.key, description.name)
self.entity_description = description
@property
def native_value(self) -> Any:
value = self.state_data.get(self.entity_description.value_key)
@@ -115,7 +119,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
async_add_entities(
[
KobraXSensor(coordinator, entry, description.key, description.name)
KobraXSensor(coordinator, entry, description)
for description in SENSORS
]
)

View File

@@ -2,7 +2,7 @@
"name": "Kobra X LAN",
"content_in_root": false,
"domains": [
"kobrax"
"kobrax_lan"
],
"homeassistant": "2024.6.0",
"iot_class": "local_polling",