Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
.venv/*
backups/*
<<<<<<< HEAD
custom_components/ha_creality_ws/__pycache__/*
ha_creality_ws.code-workspace
tools/tests/__pycache__
tools/test_files
=======
ha_creality_ws.code-workspace
tools/test_files
custom_components/ha_creality_ws/__pycache__
tools/tests/__pycache__
>>>>>>> 909e8e9f4f022889b6ae4043365fc0b54a8135a9
.agent
.issues
conductor
30 changes: 24 additions & 6 deletions custom_components/ha_creality_ws/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,9 @@ async def _ensure_stream_configured(self) -> None:
)
return

# Configure stream source with Creality format
go2rtc_src = f"webrtc:{self._upstream_signaling_url}#format=creality"
# Configure stream source
# Note: go2rtc 1.9.14+ compatibility - use standard webrtc source
go2rtc_src = f"webrtc:{self._upstream_signaling_url}"

try:
# Check if stream already exists
Expand Down Expand Up @@ -751,7 +752,10 @@ async def async_handle_async_webrtc_offer(
offer = WebRTCSdpOffer(sdp=offer_sdp)

# Forward offer to go2rtc and get answer
_LOGGER.debug("ha_creality_ws: forwarding offer to go2rtc for stream: %s", self._stream_name)
_LOGGER.debug(
"ha_creality_ws: forwarding offer to go2rtc for stream: %s. Offer size: %d",
self._stream_name, len(offer_sdp)
)
answer = await self._go2rtc_client.webrtc.forward_whep_sdp_offer(
source_name=self._stream_name,
offer=offer
Expand All @@ -773,13 +777,27 @@ async def async_handle_async_webrtc_offer(
_LOGGER.info("ha_creality_ws: WebRTC offer handled successfully")

except Go2RtcClientError as exc:
go2rtc_err = exc
_LOGGER.error(
"ha_creality_ws: go2rtc client error handling WebRTC offer: %s",
exc, exc_info=True
"ha_creality_ws: go2rtc client error handling WebRTC offer for stream '%s': %s",
self._stream_name, go2rtc_err, exc_info=True
)

# Attempt recovery by invalidating the stream to force reconfiguration next time
if self._stream_name:
_LOGGER.warning("ha_creality_ws: Invalidating stream '%s' due to go2rtc error", self._stream_name)
try:
await self._go2rtc_client.streams.delete(self._stream_name)
except Exception as cleanup_exc:
_LOGGER.debug(
"ha_creality_ws: error deleting stream '%s' during cleanup: %s",
self._stream_name, cleanup_exc,
)
self._stream_name = None

send_message(
self._wrap_send_message(
{"type": "error", "message": f"go2rtc error: {exc}"}
{"type": "error", "message": f"go2rtc error: {go2rtc_err}"}
)
)
except Exception as exc:
Expand Down
50 changes: 50 additions & 0 deletions custom_components/ha_creality_ws/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,49 @@ def extra_state_attributes(self) -> dict[str, Any]:
}


class KActiveFilamentSensor(KEntity, SensorEntity):
"""Sensor reporting which CFS slot or external filament is currently selected."""

def __init__(self, coordinator):
super().__init__(coordinator, "Active Filament Slot", "active_filament_slot")
self._attr_icon = "mdi:printer-3d-nozzle"

@property
def native_value(self) -> str | None:
if self._should_zero():
return None
data = self.coordinator.data or {}
boxes = data.get("boxsInfo", {}).get("materialBoxs", [])
for box in boxes:
box_type = box.get("type", 0)
for slot in box.get("materials", []):
if slot.get("selected"):
if box_type == 1:
return "External"
slot_id = slot.get("id", 0)
box_id = box.get("id", 0)
return f"Box {box_id} Slot {slot_id + 1}"
return None

@property
def extra_state_attributes(self) -> dict[str, Any]:
if self._should_zero():
return {}
data = self.coordinator.data or {}
boxes = data.get("boxsInfo", {}).get("materialBoxs", [])
for box in boxes:
for slot in box.get("materials", []):
if slot.get("selected"):
vendor = slot.get("vendor", "Generic")
name = slot.get("name") or slot.get("type", "Unknown")
return {
"filament": f"{vendor} {name}",
"color": slot.get("color"),
"percent": slot.get("percent"),
}
return {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.info("Setting up sensor platform for entry: %s", entry.entry_id)
coord = hass.data[DOMAIN][entry.entry_id]
Expand Down Expand Up @@ -819,6 +862,13 @@ def add_cfs_entities():
else:
_LOGGER.debug("External box found but has no materials")

# Active filament slot sensor (one per printer, added once)
active_uid = "active_filament_slot"
if active_uid not in added_cfs_uids:
new_ents.append(KActiveFilamentSensor(coord))
added_cfs_uids.add(active_uid)
_LOGGER.debug("Registered new CFS UID: %s", active_uid)

_LOGGER.debug("add_cfs_entities prepared %d new entities. Total tracked UIDs: %d", len(new_ents), len(added_cfs_uids))
return new_ents

Expand Down
8 changes: 6 additions & 2 deletions custom_components/ha_creality_ws/www/k_cfs_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,14 @@ class KCFSCard extends HTMLElement {
var(--spool-color) var(--spool-pct),
rgba(var(--rgb-primary-text-color), 0.08) 0
);
mask: radial-gradient(circle closest-side at 50% 50%, transparent 33px, black 34px);
-webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 33px, black 34px);
}

.ring-inner {
width: 66px;
height: 66px;
background: var(--card-background-color);
background: transparent;
border-radius: 50%;
position: relative;
z-index: 2;
Expand Down Expand Up @@ -365,13 +367,15 @@ class KCFSCard extends HTMLElement {
var(--spool-color) var(--spool-pct),
rgba(var(--rgb-primary-text-color), 0.08) 0
);
mask: radial-gradient(circle closest-side at 50% 50%, transparent 12px, black 13px);
-webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 12px, black 13px);
}

.spool-mini::after {
content: '';
position: absolute;
inset: 4px;
background: var(--card-background-color);
background: transparent;
border-radius: 50%;
z-index: 1;
}
Expand Down
10 changes: 3 additions & 7 deletions custom_components/ha_creality_ws/www/k_printer_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,7 @@ class KPrinterCard extends HTMLElement {
:host { --row-xpad: 6px; }

.card {
border-radius: var(--ha-card-border-radius, 12px);
background: var(--card-background-color);
color: var(--primary-text-color);
box-shadow: var(--ha-card-box-shadow, 0 2px 4px rgba(0,0,0,.2));
padding: 10px var(--row-xpad) 10px var(--row-xpad);
display: grid;
grid-template-rows: auto auto;
Expand Down Expand Up @@ -304,7 +301,7 @@ class KPrinterCard extends HTMLElement {

/* action chips – align right edge to telemetry via the same side padding */
.chips {
display:flex; gap:8px; justify-content:flex-end; flex-wrap:nowrap;
display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap;
padding: 0 var(--row-xpad);
}
.chip {
Expand Down Expand Up @@ -335,11 +332,10 @@ class KPrinterCard extends HTMLElement {
.telemetry {
display:flex;
gap:6px;
justify-content:center; /* was: flex-start */
flex-wrap:nowrap;
justify-content:center;
flex-wrap:wrap;
padding: 0 var(--row-xpad);
min-width:0;
overflow:hidden;
}
.pill {
display:inline-flex; align-items:center; gap:6px;
Expand Down
1 change: 1 addition & 0 deletions tools/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ numpy>=1.24
websockets>=12.0
# Optional for MJPEG streaming
Pillow>=10.0
pytest-asyncio>=0.23
65 changes: 49 additions & 16 deletions tools/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import sys
from pathlib import Path
import types
from typing import Optional
from unittest.mock import MagicMock

# Ensure repository root is on sys.path so `custom_components` imports work
ROOT = Path(__file__).resolve().parents[2]
Expand All @@ -11,20 +14,39 @@
if str(pkg_root) not in sys.path:
sys.path.insert(0, str(pkg_root))

import types
pkg_name = "ha_creality_ws"
full_pkg = types.ModuleType(pkg_name)
# Mark as namespace/package so submodules import from filesystem
full_pkg.__path__ = [str(pkg_root / pkg_name)]
sys.modules["custom_components.ha_creality_ws"] = full_pkg
if "custom_components.ha_creality_ws" not in sys.modules:
full_pkg = types.ModuleType(pkg_name)
# Mark as namespace/package so submodules import from filesystem
full_pkg.__path__ = [str(pkg_root / pkg_name)]
sys.modules["custom_components.ha_creality_ws"] = full_pkg

# Provide a minimal stub for Home Assistant's DataUpdateCoordinator
# --- MOCK HOME ASSISTANT ---
ha_mod = types.ModuleType("homeassistant")
helpers_mod = types.ModuleType("homeassistant.helpers")
uc_mod = types.ModuleType("homeassistant.helpers.update_coordinator")
core_mod = types.ModuleType("homeassistant.core")
components_mod = types.ModuleType("homeassistant.components")
helpers_entity_mod = types.ModuleType("homeassistant.helpers.entity")

# Mock HomeAssistant class and callback decorator
class HomeAssistant:
pass
core_mod.HomeAssistant = HomeAssistant
def callback(func):
return func
core_mod.callback = callback

setattr(ha_mod, "core", core_mod)
setattr(ha_mod, "helpers", helpers_mod)
setattr(ha_mod, "components", components_mod)

from typing import Optional
sys.modules["homeassistant"] = ha_mod
sys.modules["homeassistant.core"] = core_mod
sys.modules["homeassistant.helpers"] = helpers_mod
sys.modules["homeassistant.components"] = components_mod

# --- MOCK DataUpdateCoordinator ---
class DataUpdateCoordinator: # type: ignore
def __init__(self, hass, logger=None, name: Optional[str] = None, update_interval=None):
self.hass = hass
Expand All @@ -40,24 +62,36 @@ def async_update_listeners(self):
def __class_getitem__(cls, item):
return cls

setattr(uc_mod, "DataUpdateCoordinator", DataUpdateCoordinator)
class CoordinatorEntity:
def __init__(self, coordinator):
self.coordinator = coordinator

setattr(uc_mod, "DataUpdateCoordinator", DataUpdateCoordinator)
setattr(uc_mod, "CoordinatorEntity", CoordinatorEntity)
setattr(helpers_mod, "update_coordinator", uc_mod)

setattr(ha_mod, "helpers", helpers_mod)
sys.modules["homeassistant"] = ha_mod
sys.modules["homeassistant.helpers"] = helpers_mod
sys.modules["homeassistant.helpers.update_coordinator"] = uc_mod

# Stub aiohttp_client
# --- MOCK aiohttp_client ---
aiohttp_client_mod = types.ModuleType("homeassistant.helpers.aiohttp_client")
def async_get_clientsession(hass):
return None
setattr(aiohttp_client_mod, "async_get_clientsession", async_get_clientsession)
sys.modules["homeassistant.helpers.aiohttp_client"] = aiohttp_client_mod
setattr(helpers_mod, "aiohttp_client", aiohttp_client_mod)

# Stub out custom_components.ha_creality_ws.ws_client to avoid external deps
# --- MOCK helpers.entity ---
class DeviceInfo:
def __init__(self, **kwargs):
pass
class Entity:
pass

helpers_entity_mod.DeviceInfo = DeviceInfo
helpers_entity_mod.Entity = Entity
setattr(helpers_mod, "entity", helpers_entity_mod)
sys.modules["homeassistant.helpers.entity"] = helpers_entity_mod

# --- MOCK custom_components.ha_creality_ws.ws_client ---
ws_client_mod = types.ModuleType("custom_components.ha_creality_ws.ws_client")
import asyncio, time, contextlib # noqa: E401

Expand Down Expand Up @@ -98,5 +132,4 @@ def is_connected(self) -> bool:
return False

setattr(ws_client_mod, "KClient", KClient)
sys.modules["custom_components.ha_creality_ws.ws_client"] = ws_client_mod

sys.modules["custom_components.ha_creality_ws.ws_client"] = ws_client_mod
Loading
Loading