1 Commits

Author SHA1 Message Date
Gangoke
cd189e3987 Multi-printer discovery, persistent ACE dryer settings, switch fix (v0.4.0)
- Auto-discover sibling printers on multi-printer KX-Bridge instances
  via /kx/printers and offer to add them during config flow.
- ACE dryer target temp/duration numbers now persist across HA
  restarts (RestoreNumber) instead of resetting to defaults.
- Fix a duplicate async_turn_off definition on
  KobraXBridgeSettingSwitch that shadowed the real one and crashed
  with AttributeError; restore the correct method on
  KobraXAceAutoFeedSwitch where it belonged.
2026-07-07 20:01:05 -10:00
8 changed files with 130 additions and 18 deletions

View File

@@ -38,6 +38,10 @@ The config flow asks for:
- Host: KX-Bridge host and port (example: `192.168.1.50:7125`)
- Printer name: Friendly display name in Home Assistant
### Multi-printer setups
KX-Bridge can run multiple printers from a single instance, each on its own port. After adding the first printer, the config flow checks the bridge for sibling printers and offers to add any that aren't configured yet as separate devices — no need to manually look up each port.
## Entity Overview
| Platform | Key Entities |
@@ -60,3 +64,4 @@ Slot and ACE entities are pre-created and automatically enabled/disabled based o
- Keep KX-Bridge and Home Assistant on a trusted local network.
- Camera streaming prefers the bridge H.264 endpoint (`/api/camera/h264`, MPEG-TS passthrough) on newer bridge releases, with RTSP/MJPEG fallback for older releases.
- Native WebRTC is not implemented. For WebRTC in Home Assistant, point `go2rtc` (or another WebRTC-capable add-on) to the camera source you prefer (H.264 bridge endpoint or RTSP).
- ACE dryer target temperature and duration (Number entities) now survive Home Assistant restarts.

View File

@@ -58,6 +58,13 @@ class KobraXApiClient:
async def async_check_version(self) -> dict[str, Any]:
return await self._get_json("/api/version")
async def async_get_printers(self) -> list[dict[str, Any]]:
data = await self._get_json("/kx/printers")
result = data.get("result", [])
if isinstance(result, list):
return [printer for printer in result if isinstance(printer, dict)]
raise KobraXApiError("Unexpected response for /kx/printers")
async def async_get_state(self) -> dict[str, Any]:
return await self._get_json("/api/state")

View File

@@ -5,6 +5,7 @@ import re
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .api import KobraXApiClient, KobraXApiError
@@ -29,6 +30,10 @@ def _normalize_host(host: str) -> str:
class KobraXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self) -> None:
self._primary_data: dict[str, str] = {}
self._discovered: list[dict[str, str]] = []
async def async_step_user(self, user_input=None):
errors: dict[str, str] = {}
@@ -47,13 +52,14 @@ class KobraXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
if not errors:
return self.async_create_entry(
title=printer_name,
data={
CONF_HOST: host,
CONF_PRINTER_NAME: printer_name,
},
)
self._primary_data = {
CONF_HOST: host,
CONF_PRINTER_NAME: printer_name,
}
self._discovered = await self._async_discover_other_printers(api, host)
if self._discovered:
return await self.async_step_discovered()
return self.async_create_entry(title=printer_name, data=self._primary_data)
return self.async_show_form(
step_id="user",
@@ -61,6 +67,76 @@ class KobraXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def _async_discover_other_printers(self, api: KobraXApiClient, current_host: str) -> list[dict[str, str]]:
"""Find sibling printers on the same KX-Bridge multi-printer instance."""
try:
printers = await api.async_get_printers()
except KobraXApiError:
return []
configured_hosts = {
entry.data.get(CONF_HOST) for entry in self._async_current_entries()
}
others: list[dict[str, str]] = []
seen_hosts: set[str] = set()
for printer in printers:
bridge_url = str(printer.get("bridge_url") or "").strip()
if not bridge_url:
continue
host = _normalize_host(bridge_url)
if host == current_host or host in configured_hosts or host in seen_hosts:
continue
seen_hosts.add(host)
others.append({
CONF_HOST: host,
CONF_PRINTER_NAME: str(printer.get("name") or DEFAULT_PRINTER_NAME),
})
return others
async def async_step_discovered(self, user_input=None):
if user_input is not None:
selected_hosts = set(user_input.get("printers", []))
for printer in self._discovered:
if printer[CONF_HOST] in selected_hosts:
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data=printer,
)
)
return self.async_create_entry(
title=self._primary_data[CONF_PRINTER_NAME],
data=self._primary_data,
)
options = {
printer[CONF_HOST]: f"{printer[CONF_PRINTER_NAME]} ({printer[CONF_HOST]})"
for printer in self._discovered
}
return self.async_show_form(
step_id="discovered",
data_schema=vol.Schema(
{
vol.Optional("printers", default=list(options)): cv.multi_select(options),
}
),
)
async def async_step_integration_discovery(self, discovery_info: dict[str, str]):
"""Silently add a sibling printer discovered via /kx/printers."""
host = discovery_info[CONF_HOST]
printer_name = discovery_info.get(CONF_PRINTER_NAME) or DEFAULT_PRINTER_NAME
await self.async_set_unique_id(host)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=printer_name,
data={CONF_HOST: host, CONF_PRINTER_NAME: printer_name},
)
@staticmethod
@callback
def async_get_options_flow(config_entry):

View File

@@ -9,5 +9,5 @@
"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.3.0"
"version": "0.4.0"
}

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.number import NumberEntity
from homeassistant.components.number import RestoreNumber
from .const import DOMAIN
from .entity import KobraXEntity
@@ -18,7 +18,7 @@ def _minutes_to_hhmmss(minutes: int | float) -> str:
return f"{hours:02d}:{mins:02d}:{secs:02d}"
class KobraXAceDryConfigNumber(KobraXEntity, NumberEntity):
class KobraXAceDryConfigNumber(KobraXEntity, RestoreNumber):
def __init__(
self,
coordinator,
@@ -49,6 +49,16 @@ class KobraXAceDryConfigNumber(KobraXEntity, NumberEntity):
self._attr_mode = "box"
self._attr_icon = icon
async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
last_data = await self.async_get_last_number_data()
if last_data is None or last_data.native_value is None:
return
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]
ace_cfg = cfg.setdefault(self._ace_id, {})
key = "target_temp" if self._config_type == "target_temp" else "duration"
ace_cfg[key] = int(round(last_data.native_value))
@property
def native_value(self) -> float:
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]

View File

@@ -8,6 +8,13 @@
"host": "KX-Bridge URL",
"printer_name": "Printer name"
}
},
"discovered": {
"title": "Other printers found",
"description": "This KX-Bridge instance manages more than one printer. Select any additional printers to add now.",
"data": {
"printers": "Printers to add"
}
}
},
"error": {

View File

@@ -51,6 +51,14 @@ class KobraXAceAutoFeedSwitch(KobraXEntity, SwitchEntity):
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
async def async_turn_off(self, **kwargs) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
await api.async_set_ace_auto_feed(self._ace_id, False)
self._apply_optimistic_state(False)
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
class KobraXBridgeSettingSwitch(KobraXEntity, SwitchEntity):
def __init__(self, coordinator, entry, key: str, name: str, setting_key: str, icon: str) -> None:
@@ -103,14 +111,6 @@ class KobraXBridgeSettingSwitch(KobraXEntity, SwitchEntity):
async def async_turn_off(self, **kwargs) -> None:
await self._set_state(False)
async def async_turn_off(self, **kwargs) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
await api.async_set_ace_auto_feed(self._ace_id, False)
self._apply_optimistic_state(False)
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
class KobraXAceDryerSwitch(KobraXEntity, SwitchEntity):
def __init__(self, coordinator, entry, ace_id: int) -> None:

View File

@@ -8,6 +8,13 @@
"host": "KX-Bridge URL",
"printer_name": "Printer name"
}
},
"discovered": {
"title": "Other printers found",
"description": "This KX-Bridge instance manages more than one printer. Select any additional printers to add now.",
"data": {
"printers": "Printers to add"
}
}
},
"error": {