mirror of
https://github.com/gangoke/kobrax-lan-hass-component.git
synced 2026-07-20 01:12:35 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd189e3987 |
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user