From 2a5e9d617e648a685c5a32d1eb6c1bc5a61e2026 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 16:46:30 +0100 Subject: [PATCH 01/18] HA Integration --- .../custom_components/spoolmansync/README.md | 22 +++ .../spoolmansync/__init__.py | 63 +++++++ .../spoolmansync/config_flow.py | 38 +++++ .../spoolmansync/manifest.json | 11 ++ .../custom_components/spoolmansync/select.py | 161 ++++++++++++++++++ .../custom_components/spoolmansync/sensor.py | 111 ++++++++++++ 6 files changed, 406 insertions(+) create mode 100644 homeassistant/custom_components/spoolmansync/README.md create mode 100644 homeassistant/custom_components/spoolmansync/__init__.py create mode 100644 homeassistant/custom_components/spoolmansync/config_flow.py create mode 100644 homeassistant/custom_components/spoolmansync/manifest.json create mode 100644 homeassistant/custom_components/spoolmansync/select.py create mode 100644 homeassistant/custom_components/spoolmansync/sensor.py diff --git a/homeassistant/custom_components/spoolmansync/README.md b/homeassistant/custom_components/spoolmansync/README.md new file mode 100644 index 0000000..ca56740 --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/README.md @@ -0,0 +1,22 @@ +# SpoolmanSync Home Assistant Integration + +This integration allows you to manage your Bambu Lab AMS tray assignments directly from Home Assistant. + +## Features + +- **AMS Tray Entities**: Creates `select` entities for each AMS tray (and external spool) discovered via SpoolmanSync. +- **Spool Assignment**: Change the assigned spool for any tray using the `select` entity. +- **Spool Info**: Provides sensors with detailed information about the currently assigned spool (vendor, material, remaining weight, etc.). + +## Installation + +1. Copy the `custom_components/spoolmansync` directory to your Home Assistant `custom_components` folder. +2. Restart Home Assistant. +3. Go to **Settings** -> **Devices & Services** -> **Add Integration**. +4. Search for **SpoolmanSync**. +5. Enter your SpoolmanSync URL (e.g., `http://192.168.0.34:3000`). + +## Requirements + +- **SpoolmanSync** must be running and accessible from Home Assistant. +- **ha-bambulab** integration must be installed and configured in Home Assistant (as SpoolmanSync relies on it for printer discovery). diff --git a/homeassistant/custom_components/spoolmansync/__init__.py b/homeassistant/custom_components/spoolmansync/__init__.py new file mode 100644 index 0000000..e4f4ca4 --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/__init__.py @@ -0,0 +1,63 @@ +import logging +import asyncio +from datetime import timedelta +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "spoolmansync" +PLATFORMS = [Platform.SELECT, Platform.SENSOR] + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SpoolmanSync from a config entry.""" + url = entry.data[CONF_URL] + session = async_get_clientsession(hass) + + async def async_get_data(): + try: + # Fetch printers and spools from SpoolmanSync API + async with session.get(f"{url}/api/printers") as response: + if response.status != 200: + raise UpdateFailed(f"Error fetching printers: {response.status}") + printers_data = await response.json() + + async with session.get(f"{url}/api/spools") as response: + if response.status != 200: + raise UpdateFailed(f"Error fetching spools: {response.status}") + spools_data = await response.json() + + return { + "printers": printers_data.get("printers", []), + "spools": spools_data.get("spools", []) + } + except Exception as err: + raise UpdateFailed(f"Error communicating with SpoolmanSync: {err}") + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="spoolmansync", + update_method=async_get_data, + update_interval=timedelta(seconds=30), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/custom_components/spoolmansync/config_flow.py b/homeassistant/custom_components/spoolmansync/config_flow.py new file mode 100644 index 0000000..d5cd2ca --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/config_flow.py @@ -0,0 +1,38 @@ +import logging +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.const import CONF_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "spoolmansync" + +class SpoolmanSyncConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SpoolmanSync.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + url = user_input[CONF_URL].rstrip("/") + try: + session = async_get_clientsession(self.hass) + async with session.get(f"{url}/api/settings") as response: + if response.status == 200: + return self.async_create_entry(title="SpoolmanSync", data={CONF_URL: url}) + else: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Error connecting to SpoolmanSync") + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({ + vol.Required(CONF_URL, default="http://192.168.0.34:3000"): str, + }), + errors=errors, + ) diff --git a/homeassistant/custom_components/spoolmansync/manifest.json b/homeassistant/custom_components/spoolmansync/manifest.json new file mode 100644 index 0000000..bbeedd2 --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "spoolmansync", + "name": "SpoolmanSync", + "codeowners": ["@gibz104"], + "config_flow": true, + "documentation": "https://github.com/gibz104/SpoolmanSync", + "iot_class": "local_polling", + "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", + "requirements": [], + "version": "1.0.0" +} diff --git a/homeassistant/custom_components/spoolmansync/select.py b/homeassistant/custom_components/spoolmansync/select.py new file mode 100644 index 0000000..0c1e0ef --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/select.py @@ -0,0 +1,161 @@ +import logging +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up SpoolmanSync select entities.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + url = entry.data[CONF_URL] + + entities = [] + + # We assume there is at least one printer and it has AMS units + # The user asked for 4 entities: AMS Tray 1, 2, 3, 4 + # We will map these to the first printer's first AMS unit for simplicity, + # or create them based on discovered printers. + + printers = coordinator.data.get("printers", []) + if not printers: + _LOGGER.warning("No printers found in SpoolmanSync") + return + + # For each printer and each AMS unit, create tray entities + for printer in printers: + printer_name = printer.get("name", "Printer") + for ams in printer.get("ams_units", []): + ams_name = ams.get("name", "AMS") + for tray in ams.get("trays", []): + entities.append( + SpoolmanTraySelect( + coordinator, + url, + printer_name, + ams_name, + tray, + ) + ) + + # Also handle external spool if it exists + if printer.get("external_spool"): + entities.append( + SpoolmanTraySelect( + coordinator, + url, + printer_name, + "External", + printer["external_spool"], + ) + ) + + async_add_entities(entities) + +class SpoolmanTraySelect(CoordinatorEntity, SelectEntity): + """Representation of a Spoolman Tray selection.""" + + def __init__(self, coordinator, url, printer_name, ams_name, tray): + """Initialize the select entity.""" + super().__init__(coordinator) + self._url = url + self._printer_name = printer_name + self._ams_name = ams_name + self._tray_entity_id = tray["entity_id"] + self._tray_number = tray["tray_number"] + + # Unique ID based on the HA entity ID of the tray + self._attr_unique_id = f"spoolmansync_{self._tray_entity_id}" + self._attr_name = f"{printer_name} {ams_name} Tray {self._tray_number}" + if ams_name == "External": + self._attr_name = f"{printer_name} External Tray" + + @property + def options(self) -> list[str]: + """Return a list of available spools.""" + spools = self.coordinator.data.get("spools", []) + options = ["None"] + for spool in spools: + vendor = spool.get("filament", {}).get("vendor", {}).get("name", "Unknown") + material = spool.get("filament", {}).get("material", "Unknown") + name = spool.get("filament", {}).get("name", "") + spool_label = f"#{spool['id']} {vendor} {material} {name}".strip() + options.append(spool_label) + return options + + @property + def current_option(self) -> str | None: + """Return the currently selected spool.""" + spools = self.coordinator.data.get("spools", []) + for spool in spools: + active_tray = spool.get("extra", {}).get("active_tray") + if active_tray: + # Spoolman stores extra values as JSON strings + import json + try: + clean_tray_id = json.loads(active_tray) + if clean_tray_id == self._tray_entity_id: + vendor = spool.get("filament", {}).get("vendor", {}).get("name", "Unknown") + material = spool.get("filament", {}).get("material", "Unknown") + name = spool.get("filament", {}).get("name", "") + return f"#{spool['id']} {vendor} {material} {name}".strip() + except Exception: + if active_tray.strip('"') == self._tray_entity_id: + vendor = spool.get("filament", {}).get("vendor", {}).get("name", "Unknown") + material = spool.get("filament", {}).get("material", "Unknown") + name = spool.get("filament", {}).get("name", "") + return f"#{spool['id']} {vendor} {material} {name}".strip() + return "None" + + async def async_select_option(self, option: str) -> None: + """Change the selected spool.""" + session = async_get_clientsession(self.hass) + + if option == "None": + # Find current spool and unassign + current_spool_id = None + spools = self.coordinator.data.get("spools", []) + for spool in spools: + active_tray = spool.get("extra", {}).get("active_tray") + if active_tray: + import json + try: + if json.loads(active_tray) == self._tray_entity_id: + current_spool_id = spool["id"] + break + except: + if active_tray.strip('"') == self._tray_entity_id: + current_spool_id = spool["id"] + break + + if current_spool_id: + async with session.delete( + f"{self._url}/api/spools", + json={"spoolId": current_spool_id} + ) as response: + if response.status != 200: + _LOGGER.error("Failed to unassign spool: %s", await response.text()) + else: + # Extract ID from option string (e.g., "#123 Vendor Material Name") + try: + spool_id = int(option.split(" ")[0].replace("#", "")) + async with session.post( + f"{self._url}/api/spools", + json={"spoolId": spool_id, "trayId": self._tray_entity_id} + ) as response: + if response.status != 200: + _LOGGER.error("Failed to assign spool: %s", await response.text()) + except Exception as err: + _LOGGER.error("Error parsing spool ID from option %s: %s", option, err) + + await self.coordinator.async_request_refresh() diff --git a/homeassistant/custom_components/spoolmansync/sensor.py b/homeassistant/custom_components/spoolmansync/sensor.py new file mode 100644 index 0000000..dc07b03 --- /dev/null +++ b/homeassistant/custom_components/spoolmansync/sensor.py @@ -0,0 +1,111 @@ +import logging +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up SpoolmanSync sensor entities.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + printers = coordinator.data.get("printers", []) + for printer in printers: + printer_name = printer.get("name", "Printer") + for ams in printer.get("ams_units", []): + ams_name = ams.get("name", "AMS") + for tray in ams.get("trays", []): + entities.append( + SpoolmanTraySensor( + coordinator, + printer_name, + ams_name, + tray, + ) + ) + + if printer.get("external_spool"): + entities.append( + SpoolmanTraySensor( + coordinator, + printer_name, + "External", + printer["external_spool"], + ) + ) + + async_add_entities(entities) + +class SpoolmanTraySensor(CoordinatorEntity, SensorEntity): + """Representation of a Spoolman Tray sensor showing assigned spool info.""" + + def __init__(self, coordinator, printer_name, ams_name, tray): + """Initialize the sensor.""" + super().__init__(coordinator) + self._printer_name = printer_name + self._ams_name = ams_name + self._tray_entity_id = tray["entity_id"] + self._tray_number = tray["tray_number"] + + self._attr_unique_id = f"spoolmansync_sensor_{self._tray_entity_id}" + self._attr_name = f"{printer_name} {ams_name} Tray {self._tray_number} Info" + if ams_name == "External": + self._attr_name = f"{printer_name} External Tray Info" + + @property + def native_value(self) -> str | None: + """Return the state of the sensor.""" + spools = self.coordinator.data.get("spools", []) + for spool in spools: + active_tray = spool.get("extra", {}).get("active_tray") + if active_tray: + import json + try: + clean_tray_id = json.loads(active_tray) + if clean_tray_id == self._tray_entity_id: + return f"Spool #{spool['id']}" + except Exception: + if active_tray.strip('"') == self._tray_entity_id: + return f"Spool #{spool['id']}" + return "No Spool" + + @property + def extra_state_attributes(self) -> dict: + """Return entity specific state attributes.""" + spools = self.coordinator.data.get("spools", []) + for spool in spools: + active_tray = spool.get("extra", {}).get("active_tray") + if active_tray: + import json + try: + clean_tray_id = json.loads(active_tray) + if clean_tray_id == self._tray_entity_id: + return { + "spool_id": spool["id"], + "vendor": spool.get("filament", {}).get("vendor", {}).get("name"), + "material": spool.get("filament", {}).get("material"), + "filament_name": spool.get("filament", {}).get("name"), + "remaining_weight": spool.get("remaining_weight"), + "color_hex": spool.get("filament", {}).get("color_hex"), + } + except Exception: + if active_tray.strip('"') == self._tray_entity_id: + return { + "spool_id": spool["id"], + "vendor": spool.get("filament", {}).get("vendor", {}).get("name"), + "material": spool.get("filament", {}).get("material"), + "filament_name": spool.get("filament", {}).get("name"), + "remaining_weight": spool.get("remaining_weight"), + "color_hex": spool.get("filament", {}).get("color_hex"), + } + return {} From 7e2a87edd40a620ab8de5be20a1c1def3b5e05f2 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 16:47:57 +0100 Subject: [PATCH 02/18] Move files --- .../spoolmansync/README.md | 0 .../spoolmansync/__init__.py | 0 .../spoolmansync/config_flow.py | 0 .../spoolmansync/manifest.json | 0 .../spoolmansync/select.py | 0 .../spoolmansync/sensor.py | 0 repository.yaml | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename {homeassistant/custom_components => custom_components}/spoolmansync/README.md (100%) rename {homeassistant/custom_components => custom_components}/spoolmansync/__init__.py (100%) rename {homeassistant/custom_components => custom_components}/spoolmansync/config_flow.py (100%) rename {homeassistant/custom_components => custom_components}/spoolmansync/manifest.json (100%) rename {homeassistant/custom_components => custom_components}/spoolmansync/select.py (100%) rename {homeassistant/custom_components => custom_components}/spoolmansync/sensor.py (100%) diff --git a/homeassistant/custom_components/spoolmansync/README.md b/custom_components/spoolmansync/README.md similarity index 100% rename from homeassistant/custom_components/spoolmansync/README.md rename to custom_components/spoolmansync/README.md diff --git a/homeassistant/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py similarity index 100% rename from homeassistant/custom_components/spoolmansync/__init__.py rename to custom_components/spoolmansync/__init__.py diff --git a/homeassistant/custom_components/spoolmansync/config_flow.py b/custom_components/spoolmansync/config_flow.py similarity index 100% rename from homeassistant/custom_components/spoolmansync/config_flow.py rename to custom_components/spoolmansync/config_flow.py diff --git a/homeassistant/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json similarity index 100% rename from homeassistant/custom_components/spoolmansync/manifest.json rename to custom_components/spoolmansync/manifest.json diff --git a/homeassistant/custom_components/spoolmansync/select.py b/custom_components/spoolmansync/select.py similarity index 100% rename from homeassistant/custom_components/spoolmansync/select.py rename to custom_components/spoolmansync/select.py diff --git a/homeassistant/custom_components/spoolmansync/sensor.py b/custom_components/spoolmansync/sensor.py similarity index 100% rename from homeassistant/custom_components/spoolmansync/sensor.py rename to custom_components/spoolmansync/sensor.py diff --git a/repository.yaml b/repository.yaml index 4cb2c0c..52c063f 100644 --- a/repository.yaml +++ b/repository.yaml @@ -1,3 +1,3 @@ -name: SpoolmanSync Add-ons +name: SpoolmanSync url: https://github.com/gibz104/SpoolmanSync maintainer: gibz104 From 6b95c9bf2b692e4bebe39c3aa19a67d5d0061e68 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 16:49:07 +0100 Subject: [PATCH 03/18] a --- custom_components/spoolmansync/manifest.json | 3 ++- hacs.json | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 hacs.json diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index bbeedd2..4ff5ad3 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,5 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.0.0" + "version": "1.0.0", + "hacs": "2.0.0" } diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..e50e0dc --- /dev/null +++ b/hacs.json @@ -0,0 +1,7 @@ +{ + "name": "SpoolmanSync", + "content_in_root": false, + "zip_release": false, + "render_readme": true, + "country": [] +} From c01432e0274050cbfb544f374ef7aba38d982f04 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:01:11 +0100 Subject: [PATCH 04/18] card --- custom_components/spoolmansync/__init__.py | 22 ++++ custom_components/spoolmansync/select.py | 12 ++- .../spoolmansync/www/spoolmansync-card.js | 100 ++++++++++++++++++ homeassistant/dashboard_card_tile.yaml | 24 +++++ 4 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 custom_components/spoolmansync/www/spoolmansync-card.js create mode 100644 homeassistant/dashboard_card_tile.yaml diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index e4f4ca4..2c5a886 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -1,5 +1,6 @@ import logging import asyncio +import os from datetime import timedelta from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL, Platform @@ -50,10 +51,31 @@ async def async_get_data(): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + # Register custom card + await async_register_custom_card(hass) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True +async def async_register_custom_card(hass: HomeAssistant): + """Register the custom card with Home Assistant.""" + # This registers the www directory of the integration as a local resource + # accessible at /spoolmansync/local/ + www_path = hass.config.path("custom_components", DOMAIN, "www") + if not os.path.exists(www_path): + return + + hass.http.register_static_path( + f"/{DOMAIN}/local", + www_path, + cache_headers=False + ) + + # We can't automatically add it to the resources list via Python easily in modern HA + # but registering the static path makes it available. + # HACS usually handles the resource registration. + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/spoolmansync/select.py b/custom_components/spoolmansync/select.py index 0c1e0ef..4512b7b 100644 --- a/custom_components/spoolmansync/select.py +++ b/custom_components/spoolmansync/select.py @@ -6,6 +6,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -76,9 +77,16 @@ def __init__(self, coordinator, url, printer_name, ams_name, tray): # Unique ID based on the HA entity ID of the tray self._attr_unique_id = f"spoolmansync_{self._tray_entity_id}" - self._attr_name = f"{printer_name} {ams_name} Tray {self._tray_number}" + self._attr_name = f"{ams_name} Tray {self._tray_number}" if ams_name == "External": - self._attr_name = f"{printer_name} External Tray" + self._attr_name = "External Tray" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, printer_name)}, + name=printer_name, + manufacturer="SpoolmanSync", + model="Bambu Lab Printer", + ) @property def options(self) -> list[str]: diff --git a/custom_components/spoolmansync/www/spoolmansync-card.js b/custom_components/spoolmansync/www/spoolmansync-card.js new file mode 100644 index 0000000..7670489 --- /dev/null +++ b/custom_components/spoolmansync/www/spoolmansync-card.js @@ -0,0 +1,100 @@ +class SpoolmanSyncCard extends HTMLElement { + set hass(hass) { + if (!this.content) { + this.innerHTML = ` + +
+
+ `; + this.content = this.querySelector(".card-content"); + } + + const entities = this.config.entities || []; + this.content.innerHTML = ""; + + const grid = document.createElement("div"); + grid.style.display = "grid"; + grid.style.gridTemplateColumns = "repeat(2, 1fr)"; + grid.style.gap = "8px"; + + entities.forEach(entityId => { + const tile = document.createElement("ha-tile-card"); + tile.hass = hass; + tile.config = { + entity: entityId, + icon: "mdi:printer-3d-nozzle", + color: "blue" + }; + grid.appendChild(tile); + }); + + this.content.appendChild(grid); + } + + setConfig(config) { + if (!config.entities) { + throw new Error("You need to define entities"); + } + this.config = config; + } + + static getConfigElement() { + return document.createElement("spoolmansync-card-editor"); + } + + static getStubConfig() { + return { entities: [] }; + } +} + +customElements.define("spoolmansync-card", SpoolmanSyncCard); + +class SpoolmanSyncCardEditor extends HTMLElement { + set hass(hass) { + this._hass = hass; + if (!this.initialized) { + this.render(); + this.initialized = true; + } + } + + setConfig(config) { + this._config = config; + } + + render() { + this.innerHTML = ` +
+

Select AMS Tray Entities:

+
+
+ `; + + // In a real implementation, we would use ha-entity-picker + // For this demo, we'll just show a text area for entity IDs + const container = this.querySelector("#entities"); + const input = document.createElement("ha-textarea"); + input.label = "Entities (one per line)"; + input.value = (this._config.entities || []).join("\n"); + input.addEventListener("change", (ev) => { + const entities = ev.target.value.split("\n").filter(e => e.trim() !== ""); + const event = new CustomEvent("config-changed", { + detail: { config: { ...this._config, entities } }, + bubbles: true, + composed: true, + }); + this.dispatchEvent(event); + }); + container.appendChild(input); + } +} + +customElements.define("spoolmansync-card-editor", SpoolmanSyncCardEditor); + +window.customCards = window.customCards || []; +window.customCards.push({ + type: "spoolmansync-card", + name: "SpoolmanSync AMS Card", + description: "A card to manage your SpoolmanSync AMS trays", + preview: true, +}); diff --git a/homeassistant/dashboard_card_tile.yaml b/homeassistant/dashboard_card_tile.yaml new file mode 100644 index 0000000..26c46a6 --- /dev/null +++ b/homeassistant/dashboard_card_tile.yaml @@ -0,0 +1,24 @@ +type: grid +columns: 2 +square: false +cards: + - type: tile + entity: select.printer_ams_1_tray_1 + name: Tray 1 + icon: mdi:printer-3d-nozzle + color: blue + - type: tile + entity: select.printer_ams_1_tray_2 + name: Tray 2 + icon: mdi:printer-3d-nozzle + color: orange + - type: tile + entity: select.printer_ams_1_tray_3 + name: Tray 3 + icon: mdi:printer-3d-nozzle + color: green + - type: tile + entity: select.printer_ams_1_tray_4 + name: Tray 4 + icon: mdi:printer-3d-nozzle + color: purple From 88ef2144b759e77576aa28ef31fa7af4d532eb12 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:03:49 +0100 Subject: [PATCH 05/18] a --- custom_components/spoolmansync/__init__.py | 8 +++----- custom_components/spoolmansync/sensor.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index 2c5a886..305256d 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -66,11 +66,9 @@ async def async_register_custom_card(hass: HomeAssistant): if not os.path.exists(www_path): return - hass.http.register_static_path( - f"/{DOMAIN}/local", - www_path, - cache_headers=False - ) + hass.http.async_register_static_paths([ + hass.http.StaticPathConfig(f"/{DOMAIN}/local", www_path, False) + ]) # We can't automatically add it to the resources list via Python easily in modern HA # but registering the static path makes it available. diff --git a/custom_components/spoolmansync/sensor.py b/custom_components/spoolmansync/sensor.py index dc07b03..37aa0be 100644 --- a/custom_components/spoolmansync/sensor.py +++ b/custom_components/spoolmansync/sensor.py @@ -4,6 +4,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -58,9 +59,16 @@ def __init__(self, coordinator, printer_name, ams_name, tray): self._tray_number = tray["tray_number"] self._attr_unique_id = f"spoolmansync_sensor_{self._tray_entity_id}" - self._attr_name = f"{printer_name} {ams_name} Tray {self._tray_number} Info" + self._attr_name = f"{ams_name} Tray {self._tray_number} Info" if ams_name == "External": - self._attr_name = f"{printer_name} External Tray Info" + self._attr_name = "External Tray Info" + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, printer_name)}, + name=printer_name, + manufacturer="SpoolmanSync", + model="Bambu Lab Printer", + ) @property def native_value(self) -> str | None: From 64188e6fdeba7e19f2cbe7f11191a13d6c3c30f3 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:04:17 +0100 Subject: [PATCH 06/18] v --- custom_components/spoolmansync/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 4ff5ad3..21b549c 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.0.0", + "version": "1.1.0", "hacs": "2.0.0" } From 8a609c115481c571b1837a7a099bf9622c85d2db Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:08:01 +0100 Subject: [PATCH 07/18] New v --- custom_components/spoolmansync/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 21b549c..aeb8655 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.0", + "version": "1.1.1", "hacs": "2.0.0" } From ff6882f4237e9ae7194b78adbff757444f57a964 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:10:07 +0100 Subject: [PATCH 08/18] fix --- custom_components/spoolmansync/__init__.py | 10 +++++++--- custom_components/spoolmansync/manifest.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index 305256d..dbed9af 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -66,9 +66,13 @@ async def async_register_custom_card(hass: HomeAssistant): if not os.path.exists(www_path): return - hass.http.async_register_static_paths([ - hass.http.StaticPathConfig(f"/{DOMAIN}/local", www_path, False) - ]) + # Use the most compatible way to register static paths + # This works across many HA versions + hass.http.register_static_path( + f"/{DOMAIN}/local", + www_path, + False + ) # We can't automatically add it to the resources list via Python easily in modern HA # but registering the static path makes it available. diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index aeb8655..85658ec 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.1", + "version": "1.1.2", "hacs": "2.0.0" } From 7391776f5409fd0917134578cccb3029b176adfa Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:12:20 +0100 Subject: [PATCH 09/18] v --- custom_components/spoolmansync/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 85658ec..d87bb90 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.2", + "version": "1.1.3", "hacs": "2.0.0" } From 05c80f507140512128186262f246c1b7814d173c Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:17:48 +0100 Subject: [PATCH 10/18] a --- custom_components/spoolmansync/__init__.py | 23 ++++++++++---------- custom_components/spoolmansync/manifest.json | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index dbed9af..394acb9 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.components.http import StaticPathConfig # ← Add this import _LOGGER = logging.getLogger(__name__) @@ -60,23 +61,21 @@ async def async_get_data(): async def async_register_custom_card(hass: HomeAssistant): """Register the custom card with Home Assistant.""" - # This registers the www directory of the integration as a local resource - # accessible at /spoolmansync/local/ www_path = hass.config.path("custom_components", DOMAIN, "www") if not os.path.exists(www_path): + _LOGGER.debug("No www directory found for spoolmansync custom card") return - # Use the most compatible way to register static paths - # This works across many HA versions - hass.http.register_static_path( - f"/{DOMAIN}/local", - www_path, - False - ) + # Use the modern async method (accepts a list of StaticPathConfig) + await hass.http.async_register_static_paths([ + StaticPathConfig( + url_path=f"/{DOMAIN}/local", # e.g. /spoolmansync/local + path=www_path, + cache_headers=False # usually False for dev/custom cards + ) + ]) - # We can't automatically add it to the resources list via Python easily in modern HA - # but registering the static path makes it available. - # HACS usually handles the resource registration. + _LOGGER.info("Registered static path for spoolmansync custom card at /%s/local", DOMAIN) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index d87bb90..30449da 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.3", + "version": "1.1.4", "hacs": "2.0.0" } From 9a0ccbfd3183ecf547136ab5a2585e7af291541d Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:22:14 +0100 Subject: [PATCH 11/18] a --- custom_components/spoolmansync/manifest.json | 2 +- hacs.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 30449da..833aa6c 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.4", + "version": "1.1.5", "hacs": "2.0.0" } diff --git a/hacs.json b/hacs.json index e50e0dc..3c154c8 100644 --- a/hacs.json +++ b/hacs.json @@ -3,5 +3,6 @@ "content_in_root": false, "zip_release": false, "render_readme": true, - "country": [] + "country": [], + "filename": "spoolmansync-card.js" } From 949dbf75b2b9782ce4ed76be24f13979968f8ae1 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:24:07 +0100 Subject: [PATCH 12/18] logo --- custom_components/spoolmansync/brand/icon.png | Bin 0 -> 4173 bytes custom_components/spoolmansync/brand/logo.png | Bin 0 -> 12747 bytes custom_components/spoolmansync/manifest.json | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 custom_components/spoolmansync/brand/icon.png create mode 100644 custom_components/spoolmansync/brand/logo.png diff --git a/custom_components/spoolmansync/brand/icon.png b/custom_components/spoolmansync/brand/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..147cc64a0c714d347e96f972b07a055314b73d39 GIT binary patch literal 4173 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 Od7Bzp7*^`LMEnmz;he_+ literal 0 HcmV?d00001 diff --git a/custom_components/spoolmansync/brand/logo.png b/custom_components/spoolmansync/brand/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c1e10a6d2512cde325dcf376173ab21b050b4f GIT binary patch literal 12747 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 FzW_a`T(AHD literal 0 HcmV?d00001 diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 833aa6c..81fb4a1 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.5", + "version": "1.1.6", "hacs": "2.0.0" } From b72f409d42b525a43d5421d4116a76c5c4a8eb2b Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:29:33 +0100 Subject: [PATCH 13/18] fix --- README.md | 22 ++++ custom_components/spoolmansync/manifest.json | 2 +- .../spoolmansync/www/spoolmansync-card.js | 102 ++++++++++++------ 3 files changed, 92 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8885377..ef580f7 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,28 @@ SpoolmanSync automatically tracks which filament spools are loaded in your Bambu - **Webhook Integration** - Receives tray change events from Home Assistant automations - **Activity Logging** - Track all spool changes and sync events +## Home Assistant Integration (HACS) + +Manage your AMS trays directly from your Home Assistant dashboard with the SpoolmanSync integration. + +### Features +- **AMS Tray Selection**: Assign spools to trays using a simple dropdown menu. +- **Spool Information**: View detailed filament data (material, weight, color) for each tray. +- **Modern Dashboard Card**: Includes a custom Tile-based card for a clean, native look. +- **Automatic Sync**: Stays in sync with the SpoolmanSync web app and QR/NFC scans. + +### Installation via HACS +1. Ensure [HACS](https://hacs.xyz/) is installed. +2. Go to **HACS** → **Integrations**. +3. Click **⋮** (top right) → **Custom repositories**. +4. Add: `https://github.com/gibz104/SpoolmanSync` with category **Integration**. +5. Find **SpoolmanSync** in HACS and click **Download**. +6. Restart Home Assistant. +7. Go to **Settings** → **Devices & Services** → **Add Integration** and search for **SpoolmanSync**. +8. Enter your SpoolmanSync URL (e.g., `http://192.168.0.34:3000`). + +--- + ## Home Assistant Add-on (Recommended for HA OS users) If you're running Home Assistant OS or Supervised, install SpoolmanSync directly as an add-on: diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 81fb4a1..9532102 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.6", + "version": "1.1.8", "hacs": "2.0.0" } diff --git a/custom_components/spoolmansync/www/spoolmansync-card.js b/custom_components/spoolmansync/www/spoolmansync-card.js index 7670489..5dcd1be 100644 --- a/custom_components/spoolmansync/www/spoolmansync-card.js +++ b/custom_components/spoolmansync/www/spoolmansync-card.js @@ -1,41 +1,61 @@ +/** + * SpoolmanSync AMS Card + */ class SpoolmanSyncCard extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + set hass(hass) { + this._hass = hass; if (!this.content) { - this.innerHTML = ` - -
-
- `; - this.content = this.querySelector(".card-content"); + this.render(); + } else { + this.update(); } + } - const entities = this.config.entities || []; - this.content.innerHTML = ""; + setConfig(config) { + this._config = config; + } - const grid = document.createElement("div"); - grid.style.display = "grid"; - grid.style.gridTemplateColumns = "repeat(2, 1fr)"; - grid.style.gap = "8px"; + render() { + this.shadowRoot.innerHTML = ` + + +
+
+ `; + this.content = this.shadowRoot.querySelector(".grid"); + this.update(); + } + + update() { + if (!this.content || !this._config || !this._hass) return; + + const entities = this._config.entities || []; + this.content.innerHTML = ""; entities.forEach(entityId => { const tile = document.createElement("ha-tile-card"); - tile.hass = hass; + tile.hass = this._hass; tile.config = { entity: entityId, icon: "mdi:printer-3d-nozzle", color: "blue" }; - grid.appendChild(tile); + this.content.appendChild(tile); }); - - this.content.appendChild(grid); - } - - setConfig(config) { - if (!config.entities) { - throw new Error("You need to define entities"); - } - this.config = config; } static getConfigElement() { @@ -50,6 +70,11 @@ class SpoolmanSyncCard extends HTMLElement { customElements.define("spoolmansync-card", SpoolmanSyncCard); class SpoolmanSyncCardEditor extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + set hass(hass) { this._hass = hass; if (!this.initialized) { @@ -63,19 +88,25 @@ class SpoolmanSyncCardEditor extends HTMLElement { } render() { - this.innerHTML = ` + this.shadowRoot.innerHTML = ` +
-

Select AMS Tray Entities:

-
+

Enter AMS Tray Entity IDs (one per line):

+
`; - // In a real implementation, we would use ha-entity-picker - // For this demo, we'll just show a text area for entity IDs - const container = this.querySelector("#entities"); - const input = document.createElement("ha-textarea"); - input.label = "Entities (one per line)"; - input.value = (this._config.entities || []).join("\n"); + const input = this.shadowRoot.querySelector("ha-textarea"); input.addEventListener("change", (ev) => { const entities = ev.target.value.split("\n").filter(e => e.trim() !== ""); const event = new CustomEvent("config-changed", { @@ -85,7 +116,6 @@ class SpoolmanSyncCardEditor extends HTMLElement { }); this.dispatchEvent(event); }); - container.appendChild(input); } } @@ -98,3 +128,9 @@ window.customCards.push({ description: "A card to manage your SpoolmanSync AMS trays", preview: true, }); + +console.info( + "%c SPOOLMANSYNC-CARD %c 1.1.8 ", + "color: white; background: #03a9f4; font-weight: 700;", + "color: #03a9f4; background: white; font-weight: 700;" +); From c398e6e0e435b3dd2476cb724aee933c76c3175a Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:34:21 +0100 Subject: [PATCH 14/18] card --- custom_components/spoolmansync/manifest.json | 2 +- .../spoolmansync/www/spoolmansync-card.js | 85 +++++++++++++------ 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 9532102..9240fc4 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.1.8", + "version": "1.2.0", "hacs": "2.0.0" } diff --git a/custom_components/spoolmansync/www/spoolmansync-card.js b/custom_components/spoolmansync/www/spoolmansync-card.js index 5dcd1be..c2793f7 100644 --- a/custom_components/spoolmansync/www/spoolmansync-card.js +++ b/custom_components/spoolmansync/www/spoolmansync-card.js @@ -23,18 +23,13 @@ class SpoolmanSyncCard extends HTMLElement { render() { this.shadowRoot.innerHTML = ` - -
-
+
`; this.content = this.shadowRoot.querySelector(".grid"); this.update(); @@ -43,16 +38,25 @@ class SpoolmanSyncCard extends HTMLElement { update() { if (!this.content || !this._config || !this._hass) return; - const entities = this._config.entities || []; + const trays = [ + { id: this._config.tray1, name: "Tray 1", color: "blue" }, + { id: this._config.tray2, name: "Tray 2", color: "orange" }, + { id: this._config.tray3, name: "Tray 3", color: "green" }, + { id: this._config.tray4, name: "Tray 4", color: "purple" } + ].filter(t => t.id && t.id !== ""); + this.content.innerHTML = ""; - entities.forEach(entityId => { + trays.forEach(tray => { const tile = document.createElement("ha-tile-card"); tile.hass = this._hass; tile.config = { - entity: entityId, + entity: tray.id, + name: tray.name, icon: "mdi:printer-3d-nozzle", - color: "blue" + color: tray.color, + vertical: false, + features_position: "bottom" }; this.content.appendChild(tile); }); @@ -63,7 +67,7 @@ class SpoolmanSyncCard extends HTMLElement { } static getStubConfig() { - return { entities: [] }; + return { tray1: "", tray2: "", tray3: "", tray4: "" }; } } @@ -92,29 +96,54 @@ class SpoolmanSyncCardEditor extends HTMLElement {
-

Enter AMS Tray Entity IDs (one per line):

- + + + +
`; - const input = this.shadowRoot.querySelector("ha-textarea"); - input.addEventListener("change", (ev) => { - const entities = ev.target.value.split("\n").filter(e => e.trim() !== ""); - const event = new CustomEvent("config-changed", { - detail: { config: { ...this._config, entities } }, - bubbles: true, - composed: true, + this.shadowRoot.querySelectorAll("ha-entity-picker").forEach(picker => { + picker.addEventListener("value-changed", (ev) => { + const field = picker.getAttribute("data-config"); + const value = ev.detail.value; + const event = new CustomEvent("config-changed", { + detail: { config: { ...this._config, [field]: value } }, + bubbles: true, + composed: true, + }); + this.dispatchEvent(event); }); - this.dispatchEvent(event); }); } } @@ -130,7 +159,7 @@ window.customCards.push({ }); console.info( - "%c SPOOLMANSYNC-CARD %c 1.1.8 ", + "%c SPOOLMANSYNC-CARD %c 1.2.0 ", "color: white; background: #03a9f4; font-weight: 700;", "color: #03a9f4; background: white; font-weight: 700;" ); From 1535d4bfc6064825ccedcffdf08f8dc40272a609 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:42:16 +0100 Subject: [PATCH 15/18] a --- custom_components/spoolmansync/__init__.py | 19 +++++++------------ custom_components/spoolmansync/manifest.json | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index 394acb9..37b3ae7 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -7,7 +7,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.components.http import StaticPathConfig # ← Add this import _LOGGER = logging.getLogger(__name__) @@ -63,19 +62,15 @@ async def async_register_custom_card(hass: HomeAssistant): """Register the custom card with Home Assistant.""" www_path = hass.config.path("custom_components", DOMAIN, "www") if not os.path.exists(www_path): - _LOGGER.debug("No www directory found for spoolmansync custom card") return - # Use the modern async method (accepts a list of StaticPathConfig) - await hass.http.async_register_static_paths([ - StaticPathConfig( - url_path=f"/{DOMAIN}/local", # e.g. /spoolmansync/local - path=www_path, - cache_headers=False # usually False for dev/custom cards - ) - ]) - - _LOGGER.info("Registered static path for spoolmansync custom card at /%s/local", DOMAIN) + # Use a versioned path to force browser cache refresh + version = "1.2.1" + hass.http.register_static_path( + f"/{DOMAIN}/local/{version}", + www_path, + False + ) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 9240fc4..3afa685 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.2.0", + "version": "1.2.1", "hacs": "2.0.0" } From c175bec9a078d16c5b2aebdfe9d8fe781e5b6067 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:46:04 +0100 Subject: [PATCH 16/18] a --- custom_components/spoolmansync/__init__.py | 16 +++++++++++----- custom_components/spoolmansync/manifest.json | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/custom_components/spoolmansync/__init__.py b/custom_components/spoolmansync/__init__.py index 37b3ae7..7136bb8 100644 --- a/custom_components/spoolmansync/__init__.py +++ b/custom_components/spoolmansync/__init__.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.components.http import StaticPathConfig _LOGGER = logging.getLogger(__name__) @@ -66,11 +67,16 @@ async def async_register_custom_card(hass: HomeAssistant): # Use a versioned path to force browser cache refresh version = "1.2.1" - hass.http.register_static_path( - f"/{DOMAIN}/local/{version}", - www_path, - False - ) + url_path = f"/{DOMAIN}/local/{version}" + + # Register using the modern async API (accepts a list of StaticPathConfig) + await hass.http.async_register_static_paths([ + StaticPathConfig( + url_path=url_path, + path=www_path, + cache_headers=False # or True, depending on whether you want aggressive caching + ) + ]) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index 3afa685..d8dc84b 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.2.1", + "version": "1.2.2", "hacs": "2.0.0" } From 971e708222e30ae7d7c1b93acbf04c8ae6832018 Mon Sep 17 00:00:00 2001 From: Siebe Date: Sun, 15 Feb 2026 17:53:30 +0100 Subject: [PATCH 17/18] aaa --- custom_components/spoolmansync/manifest.json | 2 +- .../spoolmansync/www/spoolmansync-card.js | 52 +++++++++++-------- homeassistant/dashboard_card_tile.yaml | 29 ++--------- 3 files changed, 37 insertions(+), 46 deletions(-) diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index d8dc84b..f90223d 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.2.2", + "version": "1.2.3", "hacs": "2.0.0" } diff --git a/custom_components/spoolmansync/www/spoolmansync-card.js b/custom_components/spoolmansync/www/spoolmansync-card.js index c2793f7..73ca254 100644 --- a/custom_components/spoolmansync/www/spoolmansync-card.js +++ b/custom_components/spoolmansync/www/spoolmansync-card.js @@ -18,6 +18,9 @@ class SpoolmanSyncCard extends HTMLElement { setConfig(config) { this._config = config; + if (this.content) { + this.update(); + } } render() { @@ -35,9 +38,11 @@ class SpoolmanSyncCard extends HTMLElement { this.update(); } - update() { + async update() { if (!this.content || !this._config || !this._hass) return; + this.content.innerHTML = ""; + const trays = [ { id: this._config.tray1, name: "Tray 1", color: "blue" }, { id: this._config.tray2, name: "Tray 2", color: "orange" }, @@ -45,21 +50,26 @@ class SpoolmanSyncCard extends HTMLElement { { id: this._config.tray4, name: "Tray 4", color: "purple" } ].filter(t => t.id && t.id !== ""); - this.content.innerHTML = ""; - - trays.forEach(tray => { - const tile = document.createElement("ha-tile-card"); - tile.hass = this._hass; - tile.config = { - entity: tray.id, - name: tray.name, - icon: "mdi:printer-3d-nozzle", - color: tray.color, - vertical: false, - features_position: "bottom" - }; - this.content.appendChild(tile); - }); + try { + const helpers = await window.loadCardHelpers(); + const createCardElement = helpers.createCardElement; + for (const tray of trays) { + const tileConfig = { + type: "tile", + entity: tray.id, + name: tray.name, + icon: "mdi:printer-3d-nozzle", + color: tray.color, + vertical: false, + features_position: "bottom" + }; + const tile = await createCardElement(tileConfig); + tile.hass = this._hass; + this.content.appendChild(tile); + } + } catch (e) { + console.error("Error rendering SpoolmanSync tiles:", e); + } } static getConfigElement() { @@ -103,28 +113,28 @@ class SpoolmanSyncCardEditor extends HTMLElement {
Date: Sun, 15 Feb 2026 18:08:12 +0100 Subject: [PATCH 18/18] Done --- custom_components/spoolmansync/README.md | 26 +++++++++++++++++++ custom_components/spoolmansync/manifest.json | 2 +- .../spoolmansync/www/spoolmansync-card.js | 8 +++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/custom_components/spoolmansync/README.md b/custom_components/spoolmansync/README.md index ca56740..def0c68 100644 --- a/custom_components/spoolmansync/README.md +++ b/custom_components/spoolmansync/README.md @@ -20,3 +20,29 @@ This integration allows you to manage your Bambu Lab AMS tray assignments direct - **SpoolmanSync** must be running and accessible from Home Assistant. - **ha-bambulab** integration must be installed and configured in Home Assistant (as SpoolmanSync relies on it for printer discovery). + +## Lovelace AMS Card + +A custom card is included to easily manage your AMS trays. + +### Installation + +1. Add Lovelace resource (**Settings > Dashboards > Resources > + Add Resource**): + **URL**: `/custom_components/spoolmansync/www/spoolmansync-card.js` + **Resource Type**: `JavaScript Module` + +**Remote Access (Nabu Casa/Cloudflare Tunnel):** If 404 error, copy `custom_components/spoolmansync/www/spoolmansync-card.js` to your HA `config/www/spoolmansync-card.js` and use **URL**: `/local/spoolmansync-card.js` + +2. Click **Reload Resources** (or refresh browser). + +### Example + +```yaml +type: custom:spoolmansync-card +tray1: select.p2s_22e8bj5b1400071_ams_1_tray_1 +tray2: select.p2s_22e8bj5b1400071_ams_1_tray_2 +tray3: select.p2s_22e8bj5b1400071_ams_1_tray_3 +tray4: select.p2s_22e8bj5b1400071_ams_1_tray_4 +``` + +The visual editor provides entity picker dropdowns for each tray (filtered to `select` domain). diff --git a/custom_components/spoolmansync/manifest.json b/custom_components/spoolmansync/manifest.json index f90223d..1cfcf28 100644 --- a/custom_components/spoolmansync/manifest.json +++ b/custom_components/spoolmansync/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/gibz104/SpoolmanSync/issues", "requirements": [], - "version": "1.2.3", + "version": "1.2.4", "hacs": "2.0.0" } diff --git a/custom_components/spoolmansync/www/spoolmansync-card.js b/custom_components/spoolmansync/www/spoolmansync-card.js index 73ca254..00dfafb 100644 --- a/custom_components/spoolmansync/www/spoolmansync-card.js +++ b/custom_components/spoolmansync/www/spoolmansync-card.js @@ -116,28 +116,28 @@ class SpoolmanSyncCardEditor extends HTMLElement { .hass=${this._hass} .value="${this._config?.tray1 || ""}" .label="AMS Tray 1" - .includeDomains='["select"]' + .includeDomains=${['select']} data-config="tray1" >