17 Commits

Author SHA1 Message Date
gangoke
f5b4768c88 Merge pull request 'update for kx-bridge v0.9.18' (#6) from v0.9.18 into main
Reviewed-on: https://gitea.gangoke.app/gangoke/kobrax-lan-hass-component/pulls/6
2026-06-01 05:06:52 +00:00
Gangoke
36d64b876e update for bridge v0.9.18 2026-05-31 18:51:35 -10:00
Gangoke
37549de681 camera update 2026-05-30 17:34:37 -10:00
gangoke
5cb9ae0473 Merge pull request 'ace dryer fix' (#5) from ace-fix into main
Reviewed-on: https://gitea.gangoke.app/gangoke/kobrax-lan-hass-component/pulls/5
2026-05-22 08:51:43 +00:00
Gangoke
b2f153ae26 fix: improve error handling for ACE commands and add optimistic state updates 2026-05-21 22:48:03 -10:00
copilot-swe-agent[bot]
cf3b4c1ca1 fix: surface ACE endpoint error payloads without requiring result field
Agent-Logs-Url: https://github.com/gangoke/kobrax-lan-hass-component/sessions/d19d21f5-b5a1-4db3-b891-dd8cf47f3b54

Co-authored-by: gangoke <77847204+gangoke@users.noreply.github.com>
2026-05-21 12:17:27 +00:00
copilot-swe-agent[bot]
8370153dd2 fix: support ACE auto-fill off toggle and tolerate ACE endpoint responses
Agent-Logs-Url: https://github.com/gangoke/kobrax-lan-hass-component/sessions/d19d21f5-b5a1-4db3-b891-dd8cf47f3b54

Co-authored-by: gangoke <77847204+gangoke@users.noreply.github.com>
2026-05-21 12:15:41 +00:00
copilot-swe-agent[bot]
cba7f9b707 Initial plan 2026-05-21 12:12:23 +00:00
gangoke
4e5aeb54a8 Merge pull request 'ACE entity fixes' (#4) from v0.9.13 into main
Reviewed-on: https://gitea.gangoke.app/gangoke/kobrax-lan-hass-component/pulls/4
2026-05-21 11:06:26 +00:00
Gangoke
2623f2739a ACE entity fixes 2026-05-21 01:05:51 -10:00
gangoke
ec1fbbc520 Merge pull request 'ACE2 Support + more' (#3) from v0.9.13 into main
Reviewed-on: https://gitea.gangoke.app/gangoke/kobrax-lan-hass-component/pulls/3
2026-05-21 10:44:15 +00:00
Gangoke
3fb047708e ace2 dryer chnage to toggle 2026-05-21 00:39:34 -10:00
Gangoke
13ce4d48f3 ACE2 Support + more 2026-05-21 00:31:36 -10:00
gangoke
96ccfb98ff Update README.md 2026-05-18 07:45:06 +00:00
gangoke
2cc51085e4 Update README.md 2026-05-18 05:19:59 +00:00
Gangoke
b03611913a Update README.md to enhance project documentation and clarify available entities 2026-05-17 19:18:46 -10:00
gangoke
9603d449e6 Update README.md 2026-05-17 23:14:11 +00:00
80 changed files with 1264 additions and 16684 deletions

View File

@@ -1,69 +0,0 @@
name: Build Kobrax Card
on:
workflow_dispatch:
push:
paths:
- custom_components/kobrax_lan/frontend_panel/**
- .github/workflows/build-card.yml
jobs:
build-card:
runs-on: ubuntu-latest
defaults:
run:
working-directory: custom_components/kobrax_lan/frontend_panel
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: custom_components/kobrax_lan/frontend_panel/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build card
run: npm run build_card:quick
- name: Upload card artifact
uses: actions/upload-artifact@v4
with:
name: kobrax-lan-card
path: custom_components/kobrax_lan/frontend_panel/dist/kobrax-lan-card.js
if-no-files-found: error
- name: Dispatch card repo sync
uses: actions/github-script@v7
env:
CARD_REPO_DISPATCH_TOKEN: ${{ secrets.CARD_REPO_DISPATCH_TOKEN }}
CARD_REPO_OWNER: ${{ github.repository_owner }}
CARD_REPO_NAME: kobrax-lan-hass-card
if: ${{ env.CARD_REPO_DISPATCH_TOKEN != '' }}
with:
script: |
const token = process.env.CARD_REPO_DISPATCH_TOKEN;
const owner = process.env.CARD_REPO_OWNER;
const repo = process.env.CARD_REPO_NAME;
if (!token) {
core.info('CARD_REPO_DISPATCH_TOKEN is not set; skipping card repo dispatch.');
return;
}
const octokit = github.getOctokit(token);
await octokit.request('POST /repos/{owner}/{repo}/dispatches', {
owner,
repo,
event_type: 'sync-card',
client_payload: {
source_repository: `${context.repo.owner}/${context.repo.repo}`,
run_id: context.runId,
artifact_name: 'kobrax-lan-card',
source_branch: context.ref.replace('refs/heads/', ''),
},
});

3
.gitignore vendored
View File

@@ -1,6 +1,3 @@
__pycache__/
*.pyc
.pytest_cache/
node_modules/
npm-debug.log*

View File

@@ -1,62 +1,62 @@
# Kobra X Home Assistant Component
# Kobra X LAN for Home Assistant
Home Assistant HACS integration for controlling and monitoring an Anycubic Kobra X through KX-Bridge.
Home Assistant integration for monitoring and controlling an Anycubic Kobra X through KX-Bridge.
This project was coded with AI assistance and should be reviewed before use in production.
Architecture:
- printer <-> KX-Bridge-Release <-> this integration <-> Home Assistant
- printer <-> [KX-Bridge](https://gitea.it-drui.de/viewit/KX-Bridge-Release) <-> this integration <-> Home Assistant
## Features
## Requirements
- Auto-discovered status from KX-Bridge `/api/state`
- Core printer sensors (state, temperatures, progress, file, layer/time data)
- Light control
- Print speed mode selection
- Service calls for print speed mode and target temperatures (nozzle/bed)
- Printer action buttons (pause, resume, cancel)
- Camera stream entity using the printer RTSP URL from KX-Bridge, with bridge MJPEG proxy fallback
- Camera snapshot fallback using `/api/camera/snapshot`
- Card-compat alias entities for `kobrax-lan-card` (for example `job_state`, `job_progress`, `printer_online`, `target_nozzle_temperature`)
- Running and reachable [KX-Bridge-Release](https://gitea.it-drui.de/viewit/KX-Bridge-Release)
- Bridge endpoint accessible from Home Assistant at `http://<bridge-host>:7125`
## Prerequisites
## Installation
1. KX-Bridge must be running and reachable from Home Assistant.
2. Verify KX-Bridge is accessible at `http://<bridge-host>:7125`.
### Option 1: HACS
## Installation (HACS)
1. Add this repository as a custom repository in HACS with category `Integration`.
2. Install the integration.
1. In HACS, add this repository as a custom repository (category: Integration):
`https://github.com/gangoke/kobrax-lan-hass-component`
2. Install Kobra X LAN from HACS.
3. Restart Home Assistant.
4. Add integration `Kobra X` from Settings -> Devices & Services.
4. Go to Settings -> Devices & Services -> Add Integration.
5. Search for Kobra X LAN.
### Option 2: Manual (local custom_components)
1. Copy the `kobrax_lan` folder into your Home Assistant `custom_components` directory:
`<config>/custom_components/kobrax_lan`
3. Restart Home Assistant.
4. Add Kobra X LAN from Settings -> Devices & Services.
## Configuration
The config flow asks for:
- Host: KX-Bridge host and port (example: `192.168.1.50:7125`)
- Printer name: Friendly display name
- Printer name: Friendly display name in Home Assistant
## Entity Overview
| Platform | Key Entities |
| --- | --- |
| Binary Sensor | Online, Printing, Light State |
| Sensor | State, Print State, Progress, Hotend Temperature, Target Hotend Temperature, Bed Temperature, Target Bed Temperature, Filename, Current Layer, Total Layers, Remaining Time, Print Duration, Skip Object Count, Skipped Object Count, Filament Mode, ACE Unit Count, Bridge Version, Latest Available Version, Camera Stream Mode, Slot 1..Slot 19, ACE 1..4 Dryer Status, ACE 1..4 Dryer Humidity, ACE 1..4 Dryer Current Temperature, ACE 1..4 Dryer Target Temperature, ACE 1..4 Dryer Remaining Time |
| Button | Pause Print, Resume Print, Cancel Print, Connect Bridge, Disconnect Bridge, Refresh Skip State, Apply Update (KX-Bridge) |
| Switch | Camera On Print, Web Upload Warning, ACE 1..4 Auto Fill, ACE 1..4 Dryer |
| Number | ACE 1..4 Dryer Target Temperature, ACE 1..4 Dryer Duration |
| Select | Print speed, Slot 1..Slot 19 Filament Profile |
| Light | Printer light |
| Camera | Printer camera |
| Image | G-code thumbnail |
Slot and ACE entities are pre-created and automatically enabled/disabled based on detected slot mode and ACE unit count from KX-Bridge.
## Notes
- This integration talks to KX-Bridge HTTP endpoints and does not connect directly to the printer.
- Keep KX-Bridge and Home Assistant on the same trusted network.
- Native WebRTC is not implemented in this integration. If you want WebRTC in Home Assistant, point `go2rtc` or a WebRTC-capable HA add-on at the camera entity's RTSP source.
## Frontend Card Source
The source-of-truth for the Kobrax LAN card is in:
- custom_components/kobrax_lan/frontend_panel
Build output from that folder creates:
- dist/kobrax-lan-card.js
The separate `kobrax-lan-hass-card` repository is the HACS distribution repo for the built artifact.
Automation notes:
- The build workflow uploads `kobrax-lan-card.js` as a GitHub Actions artifact.
- If you add a `CARD_REPO_DISPATCH_TOKEN` secret, the workflow also dispatches a sync event to the card repo.
- The card repo workflow expects a `SOURCE_REPO_TOKEN` secret so it can download the artifact from this repo and commit the updated `kobrax-lan-card.js` file.
- This integration communicates with KX-Bridge HTTP endpoints and does not connect directly to the printer.
- 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).

View File

@@ -9,7 +9,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .api import KobraXApiClient
from .const import CONF_HOST, DOMAIN, PLATFORMS
from .coordinator import KobraXCoordinator
from .services import async_register_services, async_unregister_services
_LOGGER = logging.getLogger(__name__)
@@ -35,10 +34,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"coordinator": coordinator,
"api": api,
"entry": entry,
"ace_dry_config": {
0: {
"target_temp": int((coordinator.data or {}).get("ace_drying", {}).get("target_temp", 45) or 45),
"duration": int((coordinator.data or {}).get("ace_drying", {}).get("duration", 240) or 240),
},
1: {
"target_temp": 45,
"duration": 240,
},
2: {
"target_temp": 45,
"duration": 240,
},
3: {
"target_temp": 45,
"duration": 240,
},
},
}
async_register_services(hass)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -47,6 +62,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not any(isinstance(value, dict) and "api" in value for value in hass.data[DOMAIN].values()):
async_unregister_services(hass)
return unload_ok

View File

@@ -1,10 +1,14 @@
from __future__ import annotations
import logging
from typing import Any
import aiohttp
_LOGGER = logging.getLogger(__name__)
class KobraXApiError(Exception):
"""Raised when communication with KX-Bridge fails."""
@@ -20,6 +24,21 @@ class KobraXApiClient:
def camera_stream_proxy_url(self) -> str:
return self._url("/api/camera/stream")
def camera_h264_proxy_url(self) -> str:
return self._url("/api/camera/h264")
async def async_h264_stream_available(self, timeout_seconds: float = 1.5) -> bool:
"""Probe whether the bridge h264 endpoint is reachable now."""
try:
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
async with self._session.get(self.camera_h264_proxy_url(), timeout=timeout) as response:
if response.status != 200:
return False
chunk = await response.content.read(1)
return bool(chunk)
except Exception:
return False
async def _get_json(self, path: str) -> dict[str, Any]:
try:
async with self._session.get(self._url(path)) as response:
@@ -42,6 +61,18 @@ class KobraXApiClient:
async def async_get_state(self) -> dict[str, Any]:
return await self._get_json("/api/state")
async def async_get_settings(self) -> dict[str, Any]:
data = await self._get_json("/api/settings")
if isinstance(data, dict):
return data
raise KobraXApiError("Unexpected response for /api/settings")
async def async_set_settings(self, payload: dict[str, Any]) -> dict[str, Any]:
data = await self._post_json("/api/settings", payload)
if isinstance(data, dict):
return data
raise KobraXApiError("Unexpected response for /api/settings")
async def async_get_files(self) -> list[dict[str, Any]]:
data = await self._get_json("/kx/files")
result = data.get("result", [])
@@ -49,6 +80,109 @@ class KobraXApiClient:
return result
raise KobraXApiError("Unexpected response for /kx/files")
async def async_get_file_objects(self, file_id: str) -> dict[str, Any]:
data = await self._get_json(f"/kx/files/{file_id}/objects")
result = data.get("result", {})
if isinstance(result, dict):
return result
raise KobraXApiError("Unexpected response for /kx/files/{id}/objects")
async def async_skip_objects(self, names: list[str]) -> dict[str, Any]:
data = await self._post_json("/kx/skip", {"names": names})
result = data.get("result")
if result is None:
raise KobraXApiError("Unexpected response for /kx/skip")
return data
async def async_skip_query(self) -> dict[str, Any]:
data = await self._post_json("/kx/skip/query", {})
result = data.get("result", {})
if isinstance(result, dict):
return result
raise KobraXApiError("Unexpected response for /kx/skip/query")
async def async_get_skip_state(self) -> dict[str, Any]:
data = await self._get_json("/kx/skip/state")
result = data.get("result", {})
if isinstance(result, dict):
return result
raise KobraXApiError("Unexpected response for /kx/skip/state")
async def async_get_filament_slots(self) -> list[dict[str, Any]]:
data = await self._get_json("/kx/filament/slots")
result = data.get("result", [])
if isinstance(result, list):
return [slot for slot in result if isinstance(slot, dict)]
raise KobraXApiError("Unexpected response for /kx/filament/slots")
async def async_get_filament_profiles(self, material_type: str | None = None) -> list[dict[str, Any]]:
params: dict[str, str] = {}
if material_type:
params["type"] = material_type
try:
async with self._session.get(self._url("/kx/filament/profiles"), params=params or None) as response:
response.raise_for_status()
data = await response.json()
except Exception as err:
raise KobraXApiError(err) from err
result = data.get("result", []) if isinstance(data, dict) else []
if isinstance(result, list):
return [profile for profile in result if isinstance(profile, dict)]
raise KobraXApiError("Unexpected response for /kx/filament/profiles")
async def async_set_filament_slot_profile(
self,
slot_index: int,
filament_id: str,
vendor: str = "",
name: str = "",
) -> dict[str, Any]:
data = await self._post_json(
f"/kx/filament/slots/{int(slot_index)}/profile",
{"id": filament_id, "vendor": vendor, "name": name},
)
if isinstance(data, dict):
return data
raise KobraXApiError("Unexpected response for /kx/filament/slots/{idx}/profile")
async def async_set_ace_auto_feed(self, ace_id: int, on: bool) -> dict[str, Any]:
data = await self._post_json("/api/ace/auto_feed", {"ace_id": ace_id, "on": on})
if data.get("error") not in (None, ""):
raise KobraXApiError(str(data["error"]))
return data
async def async_set_ace_dry(
self,
action: str,
target_temp: int | None = None,
duration: int | None = None,
ace_id: int | None = None,
) -> dict[str, Any]:
payload: dict[str, Any] = {"action": action}
if target_temp is not None:
payload["target_temp"] = int(target_temp)
if duration is not None:
payload["duration"] = int(duration)
if ace_id is not None:
payload["ace_id"] = int(ace_id)
try:
data = await self._post_json("/api/ace/dry", payload)
except KobraXApiError as err:
# Some bridge versions can return a false 502 while setDry is
# still applied successfully on the printer.
msg = str(err)
if "502" in msg and "/api/ace/dry" in msg:
_LOGGER.warning("Ignoring bridge 502 for /api/ace/dry because command may already be applied: %s", msg)
return {"result": "ok", "warning": "ignored_502"}
raise
if data.get("error") not in (None, ""):
raise KobraXApiError(str(data["error"]))
return data
async def async_pause_print(self) -> None:
await self._post_json("/printer/print/pause", {})
@@ -79,9 +213,24 @@ class KobraXApiClient:
async def async_disconnect(self) -> None:
await self._post_json("/api/disconnect", {})
async def async_restart_bridge(self) -> None:
await self._post_json("/api/restart", {})
async def async_start_camera(self) -> None:
await self._post_json("/api/camera/start", {})
async def async_stop_camera(self) -> None:
await self._post_json("/api/camera/stop", {})
async def async_check_updates(self) -> dict[str, Any]:
return await self._get_json("/api/update/check")
async def async_apply_update(self, tag: str, download_url: str) -> dict[str, Any]:
return await self._post_json(
"/api/update/apply",
{"tag": tag, "download_url": download_url},
)
async def async_get_camera_url(self) -> str | None:
data = await self._get_json("/api/camera")
url = data.get("url")

View File

@@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.helpers.entity import EntityCategory
from .const import DOMAIN
from .entity import KobraXEntity
@@ -23,6 +24,7 @@ BINARY_SENSORS: tuple[KobraXBinaryDescription, ...] = (
name="Online",
value_key="kobra_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
),
KobraXBinaryDescription(
key="printing",
@@ -36,12 +38,6 @@ BINARY_SENSORS: tuple[KobraXBinaryDescription, ...] = (
value_key="light_on",
icon="mdi:lightbulb",
),
KobraXBinaryDescription(
key="printer_online",
name="Printer Online",
value_key="kobra_state",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -4,6 +4,7 @@ from dataclasses import dataclass
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity import EntityCategory
from .api import KobraXApiError
from .const import DOMAIN
@@ -13,6 +14,7 @@ from .entity import KobraXEntity
@dataclass(frozen=True, kw_only=True)
class KobraXButtonDescription(ButtonEntityDescription):
action: str
ace_id: int | None = None # None for non-ACE buttons, 0-3 for ACE buttons
BUTTONS: tuple[KobraXButtonDescription, ...] = (
@@ -39,6 +41,7 @@ BUTTONS: tuple[KobraXButtonDescription, ...] = (
name="Connect Bridge",
icon="mdi:lan-connect",
action="connect",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
KobraXButtonDescription(
@@ -46,7 +49,29 @@ BUTTONS: tuple[KobraXButtonDescription, ...] = (
name="Disconnect Bridge",
icon="mdi:lan-disconnect",
action="disconnect",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
KobraXButtonDescription(
key="restart_bridge",
name="Restart (KX-Bridge)",
icon="mdi:restart",
action="restart",
entity_category=EntityCategory.CONFIG,
),
KobraXButtonDescription(
key="refresh_skip_state",
name="Refresh Skip State",
icon="mdi:refresh",
action="skip_query",
entity_registry_enabled_default=False,
),
KobraXButtonDescription(
key="apply_update",
name="Apply Update (KX-Bridge)",
icon="mdi:download-circle-outline",
action="apply_update",
entity_category=EntityCategory.CONFIG,
),
)
@@ -58,6 +83,25 @@ class KobraXActionButton(KobraXEntity, ButtonEntity):
super().__init__(coordinator, entry, description.key, description.name)
self.entity_description = description
@property
def available(self) -> bool:
if self.entity_description.action != "apply_update":
return super().available
update_info = self.state_data.get("update_info")
if not isinstance(update_info, dict):
return False
current = str(update_info.get("current") or "").strip()
latest = str(update_info.get("latest") or "").strip()
if current and latest and current == latest:
return False
if update_info.get("update_available") is False:
return False
return super().available
async def async_press(self) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
@@ -71,16 +115,25 @@ class KobraXActionButton(KobraXEntity, ButtonEntity):
await api.async_connect()
elif self.entity_description.action == "disconnect":
await api.async_disconnect()
elif self.entity_description.action == "restart":
await api.async_restart_bridge()
elif self.entity_description.action == "skip_query":
await api.async_skip_query()
elif self.entity_description.action == "apply_update":
await self.coordinator.async_apply_update()
await self.coordinator.async_request_refresh()
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
except Exception as err:
raise ServiceValidationError(str(err)) from err
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)
for description in BUTTONS
]
)
entities = [
KobraXActionButton(coordinator, entry, description)
for description in BUTTONS
]
async_add_entities(entities)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from homeassistant.components.camera import Camera, CameraEntityFeature
from .api import KobraXApiError
@@ -21,14 +22,27 @@ class KobraXCamera(KobraXEntity, Camera):
@property
def extra_state_attributes(self) -> dict[str, str]:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
camera_url = self.state_data.get("camera_url")
attrs = {
"camera_mjpeg_proxy_url": self.hass.data[DOMAIN][self._entry.entry_id]["api"].camera_stream_proxy_url(),
"camera_h264_proxy_url": api.camera_h264_proxy_url(),
"camera_mjpeg_proxy_url": api.camera_stream_proxy_url(),
}
if isinstance(camera_url, str) and camera_url:
attrs["camera_rtsp_url"] = camera_url
return attrs
@staticmethod
def _bridge_supports_h264_stream(version: str | None) -> bool:
"""Return True when bridge version includes /api/camera/h264 support."""
if not version:
return False
match = re.search(r"(\d+)\.(\d+)\.(\d+)", version)
if not match:
return False
major, minor, patch = (int(part) for part in match.groups())
return (major, minor, patch) >= (0, 9, 17)
async def stream_source(self) -> str | None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
@@ -36,6 +50,14 @@ class KobraXCamera(KobraXEntity, Camera):
except KobraXApiError:
pass
selected_mode = "mjpeg_proxy"
selected_url = api.camera_stream_proxy_url()
version = self.state_data.get("version")
if isinstance(version, str) and self._bridge_supports_h264_stream(version):
if await api.async_h264_stream_available():
return api.camera_h264_proxy_url()
camera_url = self.state_data.get("camera_url")
if isinstance(camera_url, str) and camera_url:
return camera_url
@@ -43,9 +65,12 @@ class KobraXCamera(KobraXEntity, Camera):
try:
camera_url = await api.async_get_camera_url()
except KobraXApiError:
return api.camera_stream_proxy_url()
return selected_url
return camera_url or api.camera_stream_proxy_url()
if isinstance(camera_url, str) and camera_url:
selected_url = camera_url
return selected_url
async def async_camera_image(self, width: int | None = None, height: int | None = None) -> bytes | None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]

View File

@@ -10,7 +10,8 @@ CONF_PRINTER_NAME = "printer_name"
DEFAULT_HOST = "localhost:7125"
DEFAULT_PRINTER_NAME = "Anycubic Kobra X"
UPDATE_INTERVAL = timedelta(seconds=5)
UPDATE_INTERVAL = timedelta(seconds=3)
UPDATE_CHECK_INTERVAL = timedelta(hours=1)
PLATFORMS = [
"sensor",
@@ -18,6 +19,8 @@ PLATFORMS = [
"light",
"select",
"button",
"switch",
"number",
"camera",
"image",
]

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
import time
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import KobraXApiClient, KobraXApiError
from .const import DOMAIN, UPDATE_INTERVAL
from .const import DOMAIN, UPDATE_CHECK_INTERVAL, UPDATE_INTERVAL
class KobraXCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@@ -18,9 +19,76 @@ class KobraXCoordinator(DataUpdateCoordinator[dict[str, Any]]):
update_interval=UPDATE_INTERVAL,
)
self.api = api
self._update_info: dict[str, Any] = {}
self._next_update_check_monotonic = 0.0
async def _async_update_data(self) -> dict[str, Any]:
try:
return await self.api.async_get_state()
state = await self.api.async_get_state()
now = time.monotonic()
if now >= self._next_update_check_monotonic:
try:
self._update_info = await self.api.async_check_updates()
except KobraXApiError:
# Keep integration polling resilient if update service is temporarily unavailable.
pass
self._next_update_check_monotonic = now + UPDATE_CHECK_INTERVAL.total_seconds()
if self._update_info:
state["update_info"] = self._update_info
try:
settings = await self.api.async_get_settings()
state["settings"] = settings
except KobraXApiError:
# Settings endpoint is only available on newer bridge versions.
pass
try:
skip_state = await self.api.async_get_skip_state()
state["skip_state"] = skip_state
except KobraXApiError:
# Skip endpoints are only available on newer bridge versions.
pass
try:
filament_slots = await self.api.async_get_filament_slots()
state["filament_slots"] = filament_slots
except KobraXApiError:
# Filament profile endpoints are only available on newer bridge versions.
pass
return state
except KobraXApiError as err:
raise UpdateFailed(str(err)) from err
async def async_check_updates(self) -> dict[str, Any]:
try:
update_info = await self.api.async_check_updates()
except KobraXApiError as err:
raise UpdateFailed(str(err)) from err
self._update_info = update_info
merged = dict(self.data or {})
merged["update_info"] = update_info
self.async_set_updated_data(merged)
return update_info
async def async_apply_update(self) -> dict[str, Any]:
update_info = self._update_info or await self.async_check_updates()
tag = str(update_info.get("tag") or "").strip()
download_url = str(update_info.get("download_url") or "").strip()
if not tag or not download_url:
raise UpdateFailed("Missing tag or download URL from update check")
try:
result = await self.api.async_apply_update(tag=tag, download_url=download_url)
except KobraXApiError as err:
raise UpdateFailed(str(err)) from err
self._update_info = {**update_info, "last_apply_result": result}
merged = dict(self.data or {})
merged["update_info"] = self._update_info
self.async_set_updated_data(merged)
return result

View File

@@ -1,138 +0,0 @@
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/strict-type-checked",
"prettier",
"plugin:prettier/recommended",
"plugin:lit/recommended",
"plugin:import/recommended",
],
plugins: ["prettier", "lit", "import"],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module", // Allows for the use of imports
experimentalDecorators: true,
emitDecoratorMetadata: true,
projectService: true,
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
},
settings: {
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./tsconfig.json"
}
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
}
},
rules: {
"@typescript-eslint/array-type": "error",
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/consistent-generic-constructors": "error",
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/no-confusing-non-null-assertion": "error",
"@typescript-eslint/no-dupe-class-members": "error",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-unnecessary-parameter-property-assignment": "error",
"@typescript-eslint/no-unused-expressions": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-use-before-define": "error",
"@typescript-eslint/parameter-properties": "error",
"@typescript-eslint/restrict-template-expressions": [
"error",
{
"allowNumber": true
}
],
"@typescript-eslint/typedef": "error",
"curly": "error",
"eqeqeq": "error",
"lit/attribute-names": "warn",
"lit/ban-attributes": "error",
"lit/lifecycle-super": "error",
"lit/no-classfield-shadowing": "error",
"lit/no-invalid-escape-sequences": "error",
"lit/no-legacy-imports": "error",
"lit/no-native-attributes": "error",
"lit/no-private-properties": "error",
"lit/no-template-arrow": "error",
"lit/no-template-bind": "error",
"lit/no-template-map": "error",
"lit/no-this-assign-in-render": "error",
"lit/no-useless-template-literals": "error",
"lit/no-value-attribute": "error",
"lit/prefer-nothing": "error",
"lit/prefer-static-styles": "error",
"lit/quoted-expressions": "error",
"lit/value-after-constraints": "error",
"import/order": [
"error",
{
"groups": [
"external",
"builtin",
"internal",
"sibling",
"parent",
"index"
]
}
],
"no-console": "warn",
"no-duplicate-imports": "error",
"no-empty-function": "warn",
"no-undef": "error",
"no-unneeded-ternary": "warn",
"no-var": "error",
"operator-assignment": "warn",
"prefer-const": "error",
"sort-imports": [
"error",
{
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false
}
]
},
overrides: [
{
files: [
"*.ts",
"**/*.ts"
],
excludedFiles: [
"./src/lib",
"./src/lib/**/*.js",
]
},
],
globals: {
customElements: "writable",
document: "writable",
history: "writable",
window: "writable",
clearInterval: "readonly",
setInterval: "readonly",
clearTimeout: "readonly",
setTimeout: "readonly",
CustomEvent: "readonly",
HTMLElement: "readonly",
Window: "readonly",
Event: "readonly",
FillMode: "readonly",
scrollTo: "readonly"
}
};

View File

@@ -1,2 +0,0 @@
node_modules/
dist/

View File

@@ -1,25 +0,0 @@
{
"printWidth": 80,
"overrides": [
{
"files": "*.ts,*.js",
"options": {
"htmlWhitespaceSensitivity": "strict"
}
},
{
"files": "*.scss",
"options": {
"parser": "scss",
"singleQuote": true,
"printWidth": 200
}
},
{
"files": "*.md",
"options": {
"printWidth": 200
}
}
]
}

View File

@@ -1,38 +0,0 @@
# Kobrax LAN Frontend Panel Source
This folder is the source-of-truth for the Kobrax LAN Lovelace card build.
## Build Card
From this folder:
```bash
npm ci
npm run build_card:quick
```
Build output:
- dist/kobrax-lan-card.js
## Export To Card Repo
After building, copy the artifact to the HACS card distribution repo:
- Windows PowerShell:
```powershell
./scripts/export-card-to-repo.ps1
```
- Linux/macOS:
```bash
./scripts/export-card-to-repo.sh
```
Default export target:
- ../../../../kobrax-lan-hass-card/kobrax-lan-card.js
Override export target by setting CARD_REPO_PATH.

View File

@@ -1,158 +0,0 @@
{
"title": "Anycubic Cloud",
"common": {
"actions": {
"cancel": "Cancel",
"pause": "Pause",
"print": "Print",
"resume": "Resume",
"yes": "Yes",
"no": "No",
"save": "Save"
},
"messages": {
"mqtt_unsupported": "This feature requires MQTT to retrieve data but unfortunately MQTT is not supported with the configured authentication mode."
}
},
"card": {
"buttons": {
"print_settings": "Print Settings",
"dry": "Dry",
"runout_refill": "Refill"
},
"configure": {
"tabs": {
"main": "Main",
"stats": "Stats",
"colours": "ACE Colour Presets"
},
"labels": {
"printer_id": "Select Printer",
"vertical": "Vertical Layout?",
"round": "Round Stats?",
"use_24hr": "Use 24hr Time?",
"show_settings_button": "Always show print settings button?",
"always_show": "Always show card?",
"temperature_unit": "Temperature Unit",
"light_entity_id": "Light Entity",
"power_entity_id": "Power Entity",
"camera_entity_id": "Camera Entity",
"scale_factor": "Scale Factor",
"slot_colors": "Slot Colour Presets"
}
},
"print_settings": {
"confirm_message": "Are you sure you want to {action} the print?",
"label_nozzle_temp": "Nozzle Temperature",
"label_hotbed_temp": "Hotbed Temperature",
"label_fan_speed": "Fan Speed",
"label_aux_fan_speed": "AUX Fan Speed",
"label_box_fan_speed": "Box Fan Speed",
"print_pause": "Pause Print",
"print_resume": "Resume Print",
"print_cancel": "Cancel Print",
"save_speed_mode": "Save Speed Mode",
"save_target_nozzle": "Save Target Nozzle",
"save_target_hotbed": "Save Target Hotbed",
"save_fan_speed": "Save Fan Speed",
"save_aux_fan_speed": "Save AUX Fan Speed",
"save_box_fan_speed": "Save Box Fan Speed"
},
"drying_settings": {
"heading": "Drying Options",
"button_preset": "Preset",
"button_stop_drying": "Stop Drying",
"button_minutes": "Mins"
},
"spool_settings": {
"heading": "Editing Slot",
"label_select_material": "Select Material",
"label_select_colour": "Manually select colour"
},
"monitored_stats": {
"ETA": "ETA",
"Elapsed": "Elapsed",
"Remaining": "Remaining",
"Status": "Status",
"Online": "Online",
"Availability": "Availability",
"Project": "Project",
"Layer": "Layer",
"Hotend": "Hotend",
"Bed": "Bed",
"T Hotend": "T Hotend",
"T Bed": "T Bed",
"Dry Status": "Dry Status",
"Dry Time": "Dry Time",
"Speed Mode": "Speed Mode",
"Fan Speed": "Fan Speed",
"Dry Status": "Dry Status",
"Dry Time": "Dry Time",
"On Time": "On Time",
"Off Time": "Off Time",
"Bottom Time": "Bottom Time",
"Model Height": "Model Height",
"Bottom Layers": "Bottom Layers",
"Z Up Height": "Z Up Height",
"Z Up Speed": "Z Up Speed",
"Z Down Speed": "Z Down Speed"
}
},
"panels": {
"initial": {
"printer_select": "Select a printer."
},
"main": {
"title": "Main",
"cards": {
"main": {
"description": "General information about the printer.",
"fields": {
"printer_name": "Name",
"printer_id": "ID",
"printer_mac": "MAC",
"printer_model": "Model",
"printer_fw_version": "FW Version",
"printer_fw_update_available": "FW Status",
"printer_online": "Online",
"printer_available": "Available",
"curr_nozzle_temp": "Current Nozzle Temperature",
"curr_hotbed_temp": "Current Hotbed Temperature",
"target_nozzle_temp": "Target Nozzle Temperature",
"target_hotbed_temp": "Target Hotbed Temperature",
"job_state": "Job State",
"job_progress": "Job Progress",
"ace_fw_version": "ACE FW Version",
"ace_fw_update_available": "ACE FW Status",
"drying_active": "ACE Drying Status",
"drying_progress": "ACE Drying Progress"
}
}
}
},
"files_cloud": {
"title": "Cloud Files",
"cards": {}
},
"files_local": {
"title": "Local Files",
"cards": {}
},
"files_udisk": {
"title": "USB Files",
"cards": {}
},
"print_save_in_cloud": {
"title": "Print (Save in user cloud)",
"cards": {}
},
"print_no_cloud_save": {
"title": "Print (No Cloud Save)",
"cards": {}
},
"debug": {
"title": "Debug",
"cards": {}
}
}
}

View File

@@ -1,37 +0,0 @@
import * as en from './languages/en.json';
import IntlMessageFormat from 'intl-messageformat';
var languages: any = {
en: en,
};
export function localize(string: string, language: string, ...args: any[]): string {
const lang = language.replace(/['"]+/g, '');
var translated: string;
try {
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
} catch (e) {
translated = string.split('.').reduce((o, i) => o[i], languages['en']);
}
if (translated === undefined) translated = string.split('.').reduce((o, i) => o[i], languages['en']);
if (!args.length) return translated;
const argObject = {};
for (let i = 0; i < args.length; i += 2) {
let key = args[i];
key = key.replace(/^{([^}]+)?}$/, '$1');
argObject[key] = args[i + 1];
}
try {
const message = new IntlMessageFormat(translated, language);
return message.format(argObject) as string;
} catch (err) {
return 'Translation ' + err;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
{
"name": "kobrax-lan-frontend-panel",
"version": "0.2.2",
"description": "kobrax lan frontend panel and card source",
"main": "src/index.ts",
"scripts": {
"check-types": "tsc --noemit",
"build": "npm run lint && npm run rollup && npm run babel",
"build:quick": "npm run rollup && npm run babel",
"build_card": "npm run lint && npm run rollup_card && npm run babel_card",
"build_card:quick": "npm run rollup_card && npm run babel_card",
"rollup": "rollup -c",
"rollup_card": "rollup -c rollup.config-card.mjs",
"babel": "npx babel dist/anycubic-cloud-panel.js --out-file dist/anycubic-cloud-panel.js",
"babel_card": "npx babel dist/kobrax-lan-card.js --out-file dist/kobrax-lan-card.js",
"eslint": "eslint src --fix -c .eslintrc.js --ignore-pattern src/lib",
"lint": "npm run eslint && npm run check-types",
"prettier": "prettier src/components/**/*.ts --write",
"start": "rollup -c --watch"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/cli": "^7.24.8",
"@babel/core": "^7.24.9",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@date-fns/utc": "^2.1.0",
"@eslint/js": "^9.7.0",
"@lit-labs/motion": "^1.0.7",
"@lit-labs/observers": "^2.0.2",
"@mdi/js": "^7.4.47",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"date-fns": "^4.1.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-lit": "^1.14.0",
"eslint-plugin-prettier": "^5.2.1",
"home-assistant-js-websocket": "^9.4.0",
"intl-messageformat": "^10.5.14",
"lit": "^3.1.4",
"modern-color": "^1.1.3",
"prettier": "^3.3.3",
"rollup": "^2.79.2",
"typescript": "^5.5.4",
"typescript-eslint": "^7.17.0"
}
}

View File

@@ -1,31 +0,0 @@
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import babel from '@rollup/plugin-babel';
import json from '@rollup/plugin-json';
export default {
plugins: [
resolve(),
commonjs({
include: 'node_modules/**'
}),
typescript(),
json(),
babel(),
terser({
ecma: 2021,
module: true,
warnings: true,
}),
],
input: 'src/components/printer_card/printer_card.ts',
output: {
file: 'dist/kobrax-lan-card.js',
format: 'iife',
sourcemap: false
},
context: 'window',
preserveEntrySignatures: 'strict',
};

View File

@@ -1,31 +0,0 @@
import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import babel from '@rollup/plugin-babel';
import json from '@rollup/plugin-json';
export default {
plugins: [
resolve(),
commonjs({
include: 'node_modules/**'
}),
typescript(),
json(),
babel(),
terser({
ecma: 2021,
module: true,
warnings: true,
}),
],
input: 'src/anycubic-cloud-panel.ts',
output: {
dir: 'dist',
format: 'iife',
sourcemap: false
},
context: 'window',
preserveEntrySignatures: 'strict',
};

View File

@@ -1,21 +0,0 @@
Param(
[string]$CardRepoPath = $env:CARD_REPO_PATH
)
if (-not $CardRepoPath) {
$CardRepoPath = "../../../../kobrax-lan-hass-card"
}
$source = Join-Path $PSScriptRoot "../dist/kobrax-lan-card.js"
$targetDir = Resolve-Path -Path $CardRepoPath -ErrorAction SilentlyContinue
if (-not $targetDir) {
throw "Card repo path not found: $CardRepoPath"
}
$target = Join-Path $targetDir.Path "kobrax-lan-card.js"
if (-not (Test-Path $source)) {
throw "Build artifact not found: $source"
}
Copy-Item -Path $source -Destination $target -Force
Write-Host "Exported card artifact to $target"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CARD_REPO_PATH="${CARD_REPO_PATH:-../../../../kobrax-lan-hass-card}"
SOURCE="$(cd "$(dirname "$0")/.." && pwd)/dist/kobrax-lan-card.js"
TARGET_DIR="$(cd "$CARD_REPO_PATH" && pwd)"
TARGET="$TARGET_DIR/kobrax-lan-card.js"
if [[ ! -f "$SOURCE" ]]; then
echo "Build artifact not found: $SOURCE" >&2
exit 1
fi
cp "$SOURCE" "$TARGET"
echo "Exported card artifact to $TARGET"

View File

@@ -1,451 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import "./views/debug/view-debug.ts";
import "./views/main/view-main.ts";
import "./views/files/view-files_cloud.ts";
import "./views/files/view-files_local.ts";
import "./views/files/view-files_udisk.ts";
import "./views/print/view-print-no_cloud_save.ts";
import "./views/print/view-print-save_in_cloud.ts";
import { DEBUG } from "./const";
import { HASSDomEvent } from "./fire_event";
import {
getPage,
getPrinterDevID,
getPrinterDevices,
getSelectedPrinter,
navigateToPage,
navigateToPrinter,
} from "./helpers";
import {
DomClickEvent,
EvtTargPrinterDevId,
HassDevice,
HassDeviceList,
HassPanel,
HassRoute,
HomeAssistant,
LitTemplateResult,
PageChangeDetail,
} from "./types";
import * as pkgjson from "../package.json";
import { localize } from "../localize/localize";
window.console.info(
`%c ANYCUBIC-PANEL %c v${pkgjson.version} `,
"color: orange; font-weight: bold; background: black",
"color: white; font-weight: bold; background: dimgray",
);
@customElement("anycubic-cloud-panel")
export class AnycubicCloudPanel extends LitElement {
@property()
public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property()
public route!: HassRoute;
@property()
public panel!: HassPanel;
@state()
private printers?: HassDeviceList;
@state()
private selectedPage: string = "main";
@state()
private selectedPrinterID: string | undefined;
@state()
private selectedPrinterDevice: HassDevice | undefined;
@state()
private language: string;
@state()
private _tabMain: string;
@state()
private _tabFilesLocal: string;
@state()
private _tabFilesUdisk: string;
@state()
private _tabFilesCloud: string;
@state()
private _tabPrintNoSave: string;
@state()
private _tabPrintSave: string;
@state()
private _tabDebug: string;
@state()
private _mainTitle: string;
@state()
private _selectPrinter: string;
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("location-changed", this._handleLocationChange);
}
public disconnectedCallback(): void {
window.removeEventListener("location-changed", this._handleLocationChange);
super.disconnectedCallback();
}
private _handleLocationChange = (): void => {
if (!window.location.pathname.includes("anycubic-cloud")) {
return;
}
this.requestUpdate();
};
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("hass") && this.hass.language !== this.language) {
this.language = this.hass.language;
this._tabMain = localize("panels.main.title", this.language);
this._tabFilesLocal = localize("panels.files_local.title", this.language);
this._tabFilesUdisk = localize("panels.files_udisk.title", this.language);
this._tabFilesCloud = localize("panels.files_cloud.title", this.language);
this._tabPrintNoSave = localize(
"panels.print_no_cloud_save.title",
this.language,
);
this._tabPrintSave = localize(
"panels.print_save_in_cloud.title",
this.language,
);
this._tabDebug = localize("panels.debug.title", this.language);
this._mainTitle = localize("title", this.language);
this._selectPrinter = localize(
"panels.initial.printer_select",
this.language,
);
}
if (changedProperties.has("route")) {
this.printers = getPrinterDevices(this.hass);
this.selectedPage = getPage(this.route);
this.selectedPrinterID = getPrinterDevID(this.route);
this.selectedPrinterDevice = getSelectedPrinter(
this.printers,
this.selectedPrinterID,
);
}
}
render(): LitTemplateResult {
return this.getInitialView();
}
renderPrinterPage(): LitTemplateResult {
return html`
<div class="header">
${this.renderToolbar()}
<ha-tabs
scrollable
attr-for-selected="page-name"
.selected=${this.selectedPage}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="main"> ${this._tabMain} </paper-tab>
<paper-tab page-name="local-files">
${this._tabFilesLocal}
</paper-tab>
<paper-tab page-name="udisk-files">
${this._tabFilesUdisk}
</paper-tab>
<paper-tab page-name="cloud-files">
${this._tabFilesCloud}
</paper-tab>
<paper-tab page-name="print-no_cloud_save">
${this._tabPrintNoSave}
</paper-tab>
<paper-tab page-name="print-save_in_cloud">
${this._tabPrintSave}
</paper-tab>
${DEBUG // eslint-disable-line @typescript-eslint/no-unnecessary-condition
? html`
<paper-tab page-name="debug"> ${this._tabDebug} </paper-tab>
`
: null}
</ha-tabs>
</div>
<div class="view">${this.getView(this.route)}</div>
`;
}
renderToolbar(): LitTemplateResult {
return html`
<div class="toolbar">
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div class="main-title">${this._mainTitle}</div>
<div class="version">v${pkgjson.version}</div>
</div>
`;
}
getInitialView(): LitTemplateResult {
if (this.selectedPrinterID) {
return this.renderPrinterPage();
} else {
return html`
<div class="header">${this.renderToolbar()}</div>
<printer-select elevation="2">
<p>${this._selectPrinter}</p>
<ul class="printers-container">
${this.printers
? Object.keys(this.printers).map(
(printerID) =>
html`<li
class="printer-select-box"
.printer_id=${printerID}
@click=${this._handlePrinterClick}
>
${this.printers ? this.printers[printerID].name : ""}
</li>`,
)
: null}
</ul>
</printer-select>
`;
}
}
getView(route: HassRoute): LitTemplateResult {
switch (this.selectedPage) {
case "local-files":
return html`
<anycubic-view-files_local
class="ac_wide_view"
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-files_local>
`;
case "udisk-files":
return html`
<anycubic-view-files_udisk
class="ac_wide_view"
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-files_udisk>
`;
case "cloud-files":
return html`
<anycubic-view-files_cloud
class="ac_wide_view"
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-files_cloud>
`;
case "print-no_cloud_save":
return html`
<anycubic-view-print-no_cloud_save
class="ac_wide_view"
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-print-no_cloud_save>
`;
case "print-save_in_cloud":
return html`
<anycubic-view-print-save_in_cloud
class="ac_wide_view"
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-print-save_in_cloud>
`;
case "main":
return html`
<anycubic-view-main
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-main>
`;
case "debug":
return html`
<anycubic-view-debug
.hass=${this.hass}
.language=${this.language}
.narrow=${this.narrow}
.route=${route}
.panel=${this.panel}
.printers=${this.printers}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
></anycubic-view-debug>
`;
default:
return html`
<ha-card header="Page not found">
<div class="card-content">
The page you are trying to reach cannot be found. Please select a
page from the menu above to continue.
</div>
</ha-card>
`;
}
}
_handlePrinterClick = (ev: DomClickEvent<EvtTargPrinterDevId>): void => {
navigateToPrinter(this, ev.currentTarget.printer_id);
this.requestUpdate();
};
handlePageSelected = (ev: HASSDomEvent<PageChangeDetail>): void => {
const newPage = ev.detail.item.getAttribute("page-name") as string;
if (newPage !== getPage(this.route)) {
navigateToPage(this, newPage);
this.requestUpdate();
} else {
scrollTo(0, 0);
}
};
static get styles(): CSSResult {
return css`
:host {
padding: 16px;
display: block;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
}
.toolbar {
height: var(--header-height);
display: flex;
align-items: center;
font-size: 20px;
padding: 0 16px;
font-weight: 400;
box-sizing: border-box;
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
ha-tabs {
margin-left: max(env(safe-area-inset-left), 24px);
margin-right: max(env(safe-area-inset-right), 24px);
--paper-tabs-selection-bar-color: var(
--app-header-selection-bar-color,
var(--app-header-text-color, #fff)
);
text-transform: uppercase;
}
.version {
font-size: 14px;
font-weight: 500;
color: rgba(var(--rgb-text-primary-color), 0.9);
}
printer-select {
padding: 16px;
display: block;
font-size: 18px;
max-width: 1024px;
margin: 0 auto;
}
.view {
height: calc(100vh - 112px);
display: flex;
justify-content: center;
}
.view > * {
min-width: 600px;
max-width: 1024px;
}
.view > *:last-child {
margin-bottom: 20px;
}
.ac_wide_view {
width: 100%;
}
.printers-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.printer-select-box {
cursor: pointer;
display: block;
min-height: 60px;
min-width: 250px;
border: 2px solid #ccc3;
border-radius: 16px;
padding: 16px;
line-height: 60px;
text-align: center;
font-weight: 900;
}
.printer-select-box:hover {
background-color: #ccc3;
border-color: #ccc9;
}
@media (max-width: 599px) {
.view > * {
min-width: 100%;
max-width: 100%;
}
}
`;
}
}

View File

@@ -1,97 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { buildCameraUrlFromEntity } from "../../../helpers";
import { HassEntity, LitTemplateResult } from "../../../types";
@customElementIfUndef("anycubic-printercard-camera_view")
export class AnycubicPrintercardCameraview extends LitElement {
@property({ attribute: "show-video" })
public showVideo?: boolean | undefined;
@property({ attribute: "toggle-video" })
public toggleVideo?: () => void;
@property({ attribute: "camera-entity" })
public cameraEntity: HassEntity | undefined;
@state()
private camImgString: string = "none";
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("showVideo") ||
changedProperties.has("cameraEntity")
) {
this.camImgString =
this.showVideo && !!this.cameraEntity
? `url('${buildCameraUrlFromEntity(this.cameraEntity)}')`
: "none";
}
}
render(): LitTemplateResult {
const stylesView = {
display: this.showVideo ? "block" : "none",
};
return html`
<div
class="ac-printercard-cameraview"
style=${styleMap(stylesView)}
@click=${this._handleToggleClick}
>
${this.showVideo ? this._renderInner() : nothing}
</div>
`;
}
private _renderInner(): LitTemplateResult {
const stylesCamera = {
"background-image": this.camImgString,
};
return html` <div
class="ac-camera-wrapper"
style=${styleMap(stylesCamera)}
></div>`;
}
private _handleToggleClick = (): void => {
if (this.toggleVideo) {
this.toggleVideo();
}
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
display: block;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
.ac-printercard-cameraview {
background-color: black;
cursor: pointer;
width: 100%;
height: 100%;
}
.ac-camera-wrapper {
width: 100%;
height: 100%;
position: relative;
background-size: cover;
background-position: center;
}
`;
}
}

View File

@@ -1,762 +0,0 @@
import { mdiCog, mdiLightbulbOff, mdiLightbulbOn, mdiPower } from "@mdi/js";
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { query } from "lit/decorators/query.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { animate, Options as motionOptions } from "@lit-labs/motion";
import { localize } from "../../../../localize/localize";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { fireEvent } from "../../../fire_event";
import {
HassDevice,
HassEntity,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
PrinterCardStatType,
TemperatureUnit,
} from "../../../types";
import {
getDefaultMonitoredStats,
getEntityState,
getEntityStateBinary,
getPrinterEntities,
getPrinterEntityIdPart,
getPrinterSensorStateObj,
isPrintStatePrinting,
printStateStatusColor,
undefinedDefault,
} from "../../../helpers";
import "../camera_view/camera_view.ts";
import "../multicolorbox_view/multicolorbox_view.ts";
import "../printer_view/printer_view.ts";
import "../stats/stats_component.ts";
import "../multicolorbox_view/multicolorbox_modal_drying.ts";
import "../multicolorbox_view/multicolorbox_modal_spool.ts";
import "../printsettings/printsettings_modal.ts";
const animOptionsCard: motionOptions = {
keyframeOptions: {
duration: 250,
direction: "normal",
easing: "ease-in-out",
},
properties: ["height", "opacity", "scale"],
};
const defaultMonitoredStats: PrinterCardStatType[] = getDefaultMonitoredStats();
@customElementIfUndef("anycubic-printercard-card")
export class AnycubicPrintercardCard extends LitElement {
@query(".ac-printer-card")
private _printerCardContainer!: HTMLElement | Window;
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "monitored-stats" })
public monitoredStats?: PrinterCardStatType[] = defaultMonitoredStats;
@property({ attribute: "selected-printer-id" })
public selectedPrinterID: string | undefined;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@property({ type: Boolean })
public round?: boolean = true;
@property({ type: Boolean })
public use_24hr?: boolean;
@property({ attribute: "show-settings-button", type: Boolean })
public showSettingsButton?: boolean;
@property({ attribute: "always-show", type: Boolean })
public alwaysShow?: boolean;
@property({ attribute: "temperature-unit", type: String })
public temperatureUnit: TemperatureUnit = TemperatureUnit.C;
@property({ attribute: "light-entity-id", type: String })
public lightEntityId?: string;
@property({ attribute: "power-entity-id", type: String })
public powerEntityId?: string;
@property({ attribute: "camera-entity-id", type: String })
public cameraEntityId?: string;
@property({ type: Boolean })
public vertical?: boolean;
@property({ attribute: "scale-factor" })
public scaleFactor?: number;
@property({ attribute: "slot-colors" })
public slotColors?: string[];
@state()
private _showVideo: boolean = false;
@state()
private cameraEntityState: HassEntity | undefined = undefined;
@state()
private isHidden: boolean = false;
@state()
private isPrinting: boolean = false;
@state()
private hiddenOverride: boolean = false;
@state()
private hasColorbox: boolean = false;
@state()
private hasSecondaryColorbox: boolean = false;
@state()
private lightIsOn: boolean = false;
@state()
private statusColor: string = "#ffc107";
@state()
private printerEntities: HassEntityInfos;
@state()
private printerEntityIdPart: string | undefined;
@state()
private progressPercent: number = 0;
@state()
private _buttonPrintSettings: string;
@state()
private _togglingLight: boolean = false;
@state()
private _togglingPower: boolean = false;
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._buttonPrintSettings = localize(
"card.buttons.print_settings",
this.language,
);
}
if (changedProperties.has("monitoredStats")) {
this.monitoredStats = undefinedDefault(
this.monitoredStats,
defaultMonitoredStats,
) as PrinterCardStatType[];
}
if (changedProperties.has("selectedPrinterID")) {
this.printerEntities = getPrinterEntities(
this.hass,
this.selectedPrinterID,
);
this.printerEntityIdPart = getPrinterEntityIdPart(this.printerEntities);
}
if (
changedProperties.has("hass") ||
changedProperties.has("alwaysShow") ||
changedProperties.has("hiddenOverride") ||
changedProperties.has("selectedPrinterID")
) {
this.progressPercent = this._percentComplete();
this.hasColorbox =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"ace_spools",
"inactive",
).state === "active";
this.hasSecondaryColorbox =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"secondary_multi_color_box_spools",
"inactive",
).state === "active";
if (this.cameraEntityId) {
this.cameraEntityState = getEntityState(this.hass, {
entity_id: this.cameraEntityId,
});
}
this.lightIsOn = getEntityStateBinary(
this.hass,
{ entity_id: this.lightEntityId ?? "" },
true,
false,
) as boolean;
const printStateString = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_state",
"unknown",
).state.toLowerCase();
this.isPrinting = isPrintStatePrinting(printStateString);
this.isHidden = !this.alwaysShow
? !this.hiddenOverride && !this.isPrinting
: false;
this.statusColor = printStateStatusColor(printStateString);
this.lightIsOn = getEntityStateBinary(
this.hass,
{ entity_id: this.lightEntityId ?? "" },
true,
false,
) as boolean;
}
}
render(): LitTemplateResult {
const classesCam = {
"ac-hidden": !this._showVideo,
};
return html`
<div class="ac-printer-card">
<div class="ac-printer-card-mainview">
${this._renderHeader()} ${this._renderPrinterContainer()}
</div>
<anycubic-printercard-camera_view
class=${classMap(classesCam)}
.showVideo=${this._showVideo}
.toggleVideo=${this._toggleVideo}
.cameraEntity=${this.cameraEntityState}
></anycubic-printercard-camera_view>
<anycubic-printercard-multicolorbox_modal_spool
.hass=${this.hass}
.language=${this.language}
.selectedPrinterDevice=${this.selectedPrinterDevice}
.slotColors=${this.slotColors}
></anycubic-printercard-multicolorbox_modal_spool>
<anycubic-printercard-printsettings_modal
.hass=${this.hass}
.language=${this.language}
.selectedPrinterDevice=${this.selectedPrinterDevice}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
></anycubic-printercard-printsettings_modal>
<anycubic-printercard-multicolorbox_modal_drying
.hass=${this.hass}
.language=${this.language}
.selectedPrinterDevice=${this.selectedPrinterDevice}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
></anycubic-printercard-multicolorbox_modal_drying>
</div>
`;
}
private _renderHeader(): LitTemplateResult {
const classesHeader = {
"ac-h-justifycenter": !(this.powerEntityId && this.lightEntityId),
};
const stylesDot = {
"background-color": this.statusColor,
};
return html`
<div class="ac-printer-card-header ${classMap(classesHeader)}">
${this.powerEntityId
? html`
<button
class="ac-printer-card-button-small"
.disabled=${this._togglingPower}
@click=${this._togglePowerEntity}
>
<ha-svg-icon .path=${mdiPower}></ha-svg-icon>
</button>
`
: nothing}
<button
class="ac-printer-card-button-name"
@click=${this._toggleHiddenOveride}
>
<div
class="ac-printer-card-header-status-dot"
style=${styleMap(stylesDot)}
></div>
<p class="ac-printer-card-header-status-text">
${this.selectedPrinterDevice?.name}
</p>
</button>
${this.lightEntityId
? html`
<button
class="ac-printer-card-button-small"
.disabled=${this._togglingLight}
@click=${this._toggleLightEntity}
>
<ha-svg-icon
.path=${this.lightIsOn ? mdiLightbulbOn : mdiLightbulbOff}
></ha-svg-icon>
</button>
`
: nothing}
</div>
`;
}
private _renderPrinterContainer(): LitTemplateResult {
const classesMain = {
"ac-card-vertical": !!this.vertical,
};
const stylesMain = {
height: this.isHidden ? "1px" : "auto",
opacity: this.isHidden ? 0.0 : 1.0,
scale: this.isHidden ? 0.0 : 1.0,
};
const stylesScaledColLeft = {
width: this.vertical
? "100%"
: this.scaleFactor
? String(50 * this.scaleFactor) + "%"
: "50%",
};
const stylesScaledColRight = {
width: this.vertical
? "100%"
: this.scaleFactor
? String(50 / this.scaleFactor) + "%"
: "50%",
};
return html`
<div
class="ac-printer-card-infocontainer ${classMap(classesMain)}"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<div
class="ac-printer-card-info-animcontainer ${classMap(classesMain)}"
style=${styleMap(stylesScaledColLeft)}
>
<anycubic-printercard-printer_view
.hass=${this.hass}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
.scaleFactor=${this.scaleFactor}
.toggleVideo=${this._toggleVideo}
></anycubic-printercard-printer_view>
${this.vertical
? html`<p class="ac-printer-card-info-vertprog">
${this.round
? Math.round(this.progressPercent)
: this.progressPercent}%
</p>`
: nothing}
</div>
<div
class="ac-printer-card-info-statscontainer ${classMap(classesMain)}"
style=${styleMap(stylesScaledColRight)}
>
<anycubic-printercard-stats-component
.hass=${this.hass}
.language=${this.language}
.monitoredStats=${this.monitoredStats}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
.progressPercent=${this.progressPercent}
.showPercent=${!this.vertical}
.round=${this.round}
.use_24hr=${this.use_24hr}
.temperatureUnit=${this.temperatureUnit}
></anycubic-printercard-stats-component>
</div>
</div>
${this._renderPrintSettingsContainer()}
${this._renderMultiColorBoxContainer()}
${this._renderSecondaryMultiColorBoxContainer()}
`;
}
private _toggleVideo = (): void => {
this._showVideo = !!(this.cameraEntityState && !this._showVideo);
};
private _renderPrintSettingsContainer(): LitTemplateResult {
const classesMain = {
"ac-card-vertical": !!this.vertical,
};
const stylesMain = {
height: this.isHidden ? "1px" : "auto",
opacity: this.isHidden ? 0.0 : 1.0,
scale: this.isHidden ? 0.0 : 1.0,
};
return this.showSettingsButton || this.isPrinting
? html`
<div
class="ac-printer-card-infocontainer ${classMap(classesMain)}"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<div
class="ac-printer-card-settingssection ${classMap(classesMain)}"
>
<button
class="ac-printer-card-button-settings"
@click=${this._openPrintSettingsModal}
>
<ha-svg-icon .path=${mdiCog}></ha-svg-icon>
${this._buttonPrintSettings}
</button>
</div>
</div>
`
: nothing;
}
private _renderMultiColorBoxContainer(): LitTemplateResult {
const classesMain = {
"ac-card-vertical": !!this.vertical,
};
const stylesMain = {
height: this.isHidden ? "1px" : "auto",
opacity: this.isHidden ? 0.0 : 1.0,
scale: this.isHidden ? 0.0 : 1.0,
};
return this.hasColorbox
? html`
<div
class="ac-printer-card-infocontainer ${classMap(classesMain)}"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<div class="ac-printer-card-mcbsection ${classMap(classesMain)}">
<anycubic-printercard-multicolorbox_view
.hass=${this.hass}
.language=${this.language}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
.box_id=${0}
></anycubic-printercard-multicolorbox_view>
</div>
</div>
`
: nothing;
}
private _renderSecondaryMultiColorBoxContainer(): LitTemplateResult {
const classesMain = {
"ac-card-vertical": !!this.vertical,
};
const stylesMain = {
height: this.isHidden ? "1px" : "auto",
opacity: this.isHidden ? 0.0 : 1.0,
scale: this.isHidden ? 0.0 : 1.0,
};
return this.hasSecondaryColorbox
? html`
<div
class="ac-printer-card-infocontainer ${classMap(classesMain)}"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<div class="ac-printer-card-mcbsection ${classMap(classesMain)}">
<anycubic-printercard-multicolorbox_view
.hass=${this.hass}
.language=${this.language}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
.box_id=${1}
></anycubic-printercard-multicolorbox_view>
</div>
</div>
`
: nothing;
}
private _openPrintSettingsModal = (): void => {
fireEvent(this._printerCardContainer, "ac-printset-modal", {
modalOpen: true,
});
};
private _toggleLightEntity = (): void => {
let targetEntityId: string | undefined = this.lightEntityId;
if (!targetEntityId && this.printerEntityIdPart) {
targetEntityId = getPrinterEntityId(
this.printerEntityIdPart,
"light",
"light",
);
}
if ((!targetEntityId || !this.hass?.states?.[targetEntityId]) && this.printerEntities) {
for (const entityId in this.printerEntities) {
if (entityId.startsWith("light.") && entityId.endsWith("_light")) {
targetEntityId = entityId;
break;
}
}
}
if (targetEntityId && this.hass?.states?.[targetEntityId]) {
this._togglingLight = true;
this.hass
.callService("homeassistant", "toggle", {
entity_id: targetEntityId,
})
.then(() => {
this._togglingLight = false;
})
.catch((_e: unknown) => {
this._togglingLight = false;
});
}
};
private _togglePowerEntity = (): void => {
if (this.powerEntityId) {
this._togglingPower = true;
this.hass
.callService("homeassistant", "toggle", {
entity_id: this.powerEntityId,
})
.then(() => {
this._togglingPower = false;
})
.catch((_e: unknown) => {
this._togglingPower = false;
});
}
};
private _toggleHiddenOveride = (): void => {
this.hiddenOverride = !this.hiddenOverride;
};
private _percentComplete(): number {
return Number(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_progress",
-1.0,
).state,
);
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
.ac-printer-card {
display: flex;
flex-direction: row;
justify-content: center;
align-items: stretch;
box-sizing: border-box;
background: var(
--ha-card-background,
var(--card-background-color, white)
);
position: relative;
overflow: hidden;
border-radius: 16px;
margin: 0px;
box-shadow: var(
--ha-card-box-shadow,
0px 2px 1px -1px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 1px 3px 0px rgba(0, 0, 0, 0.12)
);
}
.ac-printer-card-mainview {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 100%;
}
.ac-printer-card-header {
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
width: 100%;
justify-content: space-between;
}
.ac-h-justifycenter {
justify-content: center;
}
.ac-printer-card-button-small {
border: none;
outline: none;
background-color: transparent;
width: 32px;
height: 32px;
font-size: 22px;
line-height: 22px;
box-sizing: border-box;
padding: 0px;
margin-right: 24px;
margin-left: 24px;
cursor: pointer;
color: var(--primary-text-color);
}
.ac-printer-card-button-settings {
border: none;
border-radius: 6px;
outline: none;
background-color: transparent;
font-size: 18px;
box-sizing: border-box;
padding: 4px 12px;
margin-right: 24px;
margin-left: 24px;
cursor: pointer;
color: var(--primary-text-color);
}
.ac-printer-card-button-settings:hover {
background-color: #7f7f7f36;
}
.ac-printer-card-button-settings:active {
background-color: #7f7f7f5e;
}
.ac-printer-card-button-name {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: none;
outline: none;
background-color: transparent;
padding: 24px;
}
.ac-printer-card-header-status-dot {
margin: 0px 10px;
height: 10px;
width: 10px;
border-radius: 5px;
box-sizing: border-box;
}
.ac-printer-card-header-status-text {
font-weight: bold;
font-size: 22px;
margin: 0px;
color: var(--primary-text-color);
}
.ac-printer-card-infocontainer {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.ac-printer-card-infocontainer.ac-card-vertical {
flex-direction: column;
}
.ac-printer-card-info-animcontainer {
box-sizing: border-box;
padding: 0px 8px 32px 8px;
width: 50%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.ac-printer-card-info-animcontainer.ac-card-vertical {
width: 100%;
height: auto;
padding-left: 64px;
padding-right: 64px;
}
anycubic-printercard-printer_view {
width: 100%;
flex-grow: 1;
}
.ac-printer-card-info-vertprog {
width: 50%;
font-size: 36px;
text-align: center;
font-weight: bold;
}
anycubic-printercard-printer_view.ac-card-vertical {
width: auto;
}
.ac-printer-card-info-statscontainer {
box-sizing: border-box;
padding: 0px 16px 32px 8px;
width: 50%;
height: 100%;
}
.ac-printer-card-info-statscontainer.ac-card-vertical {
padding-left: 32px;
padding-right: 32px;
width: 100%;
height: auto;
}
.ac-printer-card-mcbsection {
box-sizing: border-box;
padding: 6px;
width: 100%;
height: 100%;
}
.ac-printer-card-mcbsection.ac-card-vertical {
height: auto;
}
.ac-hidden {
display: none;
}
`;
}
}

View File

@@ -1,512 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { localize } from "../../../../localize/localize";
import {
CAMERA_ENTITY_DOMAINS,
LIGHT_ENTITY_DOMAINS,
SWITCH_ENTITY_DOMAINS,
} from "../../../const";
import { HASSDomEvent, fireEvent } from "../../../fire_event";
import {
getDefaultCardConfig,
getPrinterEntities,
getPrinterEntityIdPart,
getPrinterSensorStateObj,
isLCDPrinter,
} from "../../../helpers";
import {
AnycubicCardConfig,
CalculatedTimeType,
FormChangeDetail,
HaFormBaseSchema,
HassDeviceList,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
PageChangeDetail,
PrinterCardStatType,
StatTypeACE,
StatTypeFDM,
StatTypeGeneral,
StatTypeLCD,
TemperatureUnit,
} from "../../../types";
import "../../ui/multi-select-reorder.ts";
const defaultConfig = getDefaultCardConfig();
@customElement("anycubic-printercard-configure")
export class AnycubicPrintercardConfigure extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "card-config" })
public cardConfig!: AnycubicCardConfig;
@property()
public printers!: HassDeviceList;
@state()
private configPage: string = "main";
@state()
private availableStats: object = {};
@state()
private formSchemaMain: HaFormBaseSchema[] = [];
@state()
private formSchemaColours: HaFormBaseSchema[] = [];
@state()
private printerEntities: HassEntityInfos;
@state()
private printerEntityIdPart: string | undefined;
@state()
private hasColorbox: boolean = false;
@state()
private isLCD: boolean = false;
@state()
private _tabMain: string;
@state()
private _tabStats: string;
@state()
private _tabColours: string;
@state()
private _labelPrinter_id: string;
@state()
private _labelVertical: string;
@state()
private _labelRound: string;
@state()
private _labelUse_24hr: string;
@state()
private _labelShowSettingsButton: string;
@state()
private _labelAlwaysShow: string;
@state()
private _labelTemperatureUnit: string;
@state()
private _labelLightEntityId: string;
@state()
private _labelPowerEntityId: string;
@state()
private _labelCameraEntityId: string;
@state()
private _labelScaleFactor: string;
@state()
private _labelSlotColors: string;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._tabMain = localize("card.configure.tabs.main", this.language);
this._tabStats = localize("card.configure.tabs.stats", this.language);
this._tabColours = localize("card.configure.tabs.colours", this.language);
this._labelPrinter_id = localize(
"card.configure.labels.printer_id",
this.language,
);
this._labelVertical = localize(
"card.configure.labels.vertical",
this.language,
);
this._labelRound = localize("card.configure.labels.round", this.language);
this._labelUse_24hr = localize(
"card.configure.labels.use_24hr",
this.language,
);
this._labelShowSettingsButton = localize(
"card.configure.labels.show_settings_button",
this.language,
);
this._labelAlwaysShow = localize(
"card.configure.labels.always_show",
this.language,
);
this._labelTemperatureUnit = localize(
"card.configure.labels.temperature_unit",
this.language,
);
this._labelLightEntityId = localize(
"card.configure.labels.light_entity_id",
this.language,
);
this._labelPowerEntityId = localize(
"card.configure.labels.power_entity_id",
this.language,
);
this._labelCameraEntityId = localize(
"card.configure.labels.camera_entity_id",
this.language,
);
this._labelScaleFactor = localize(
"card.configure.labels.scale_factor",
this.language,
);
this._labelSlotColors = localize(
"card.configure.labels.slot_colors",
this.language,
);
}
if (changedProperties.has("hass") || changedProperties.has("cardConfig")) {
this.printerEntities = getPrinterEntities(
this.hass,
this.cardConfig.printer_id,
);
this.printerEntityIdPart = getPrinterEntityIdPart(this.printerEntities);
this.isLCD = isLCDPrinter(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
);
this.hasColorbox =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"ace_spools",
"inactive",
).state === "active";
this.availableStats = {
...StatTypeGeneral,
...CalculatedTimeType,
};
if (this.isLCD) {
this.availableStats = {
...this.availableStats,
...StatTypeLCD,
};
} else {
this.availableStats = {
...this.availableStats,
...StatTypeFDM,
};
}
if (this.hasColorbox) {
this.availableStats = {
...this.availableStats,
...StatTypeACE,
};
}
}
if (
changedProperties.has("printers") ||
changedProperties.has("language")
) {
this.formSchemaMain = this._computeSchemaMain();
this.formSchemaColours = this._computeSchemaColours();
}
}
render(): LitTemplateResult {
return html`
<div class="ac-printer-card-configure-cont">
${this._renderMenu()} ${this._renderConfMain()}
${this._renderConfColours()} ${this._renderConfStats()}
</div>
`;
}
private _renderConfMain(): LitTemplateResult {
return this.configPage === "main"
? html`
<div class="ac-printer-card-configure-conf">
<ha-form
.hass=${this.hass}
.data=${this.cardConfig}
.schema=${this.formSchemaMain}
.computeLabel=${this._computeLabel}
@value-changed=${this._formValueChanged}
></ha-form>
</div>
`
: nothing;
}
private _renderConfStats(): LitTemplateResult {
return this.configPage === "stats"
? html`
<div class="ac-printer-card-configure-conf">
<p class="ac-cconf-label">Choose Monitored Stats</p>
<anycubic-ui-multi-select-reorder
.availableOptions=${this.availableStats}
.initialItems=${this.cardConfig.monitoredStats}
.onChange=${this._selectedStatsChanged}
></anycubic-ui-multi-select-reorder>
</div>
`
: nothing;
}
private _renderConfColours(): LitTemplateResult {
return this.configPage === "colours"
? html`
<div class="ac-printer-card-configure-conf">
<ha-form
.hass=${this.hass}
.data=${this.cardConfig}
.schema=${this.formSchemaColours}
.computeLabel=${this._computeLabel}
@value-changed=${this._formValueChanged}
></ha-form>
</div>
`
: nothing;
}
private _renderMenu(): LitTemplateResult {
return html`
<div class="header">
<ha-tabs
scrollable
attr-for-selected="page-name"
.selected=${this.configPage}
@iron-activate=${this._handlePageSelected}
>
<paper-tab page-name="main">${this._tabMain}</paper-tab>
<paper-tab page-name="stats">${this._tabStats}</paper-tab>
${this.hasColorbox
? html`<paper-tab page-name="colours">
${this._tabColours}
</paper-tab>`
: nothing}
</ha-tabs>
</div>
`;
}
private _handlePageSelected = (ev: HASSDomEvent<PageChangeDetail>): void => {
const newPage = ev.detail.item.getAttribute("page-name") as string;
if (newPage !== this.configPage) {
this.configPage = newPage;
}
};
private _selectedStatsChanged = (selected: PrinterCardStatType[]): void => {
this.cardConfig.monitoredStats = selected;
this._configChanged(this.cardConfig);
};
private _configChanged(newConfig: AnycubicCardConfig): void {
const filteredConfig = Object.keys(newConfig)
.filter((key) => newConfig[key] !== defaultConfig[key])
.reduce((fConf: AnycubicCardConfig, key: string) => {
fConf[key] = newConfig[key as keyof AnycubicCardConfig];
return fConf;
}, {});
fireEvent(this, "config-changed", { config: filteredConfig });
}
private _formValueChanged = (ev: HASSDomEvent<FormChangeDetail>): void => {
this.cardConfig = ev.detail.value;
this._configChanged(this.cardConfig);
};
private _computeLabel = (schema: HaFormBaseSchema): string => {
switch (schema.name) {
case "printer_id":
return this._labelPrinter_id;
case "vertical":
return this._labelVertical;
case "round":
return this._labelRound;
case "use_24hr":
return this._labelUse_24hr;
case "showSettingsButton":
return this._labelShowSettingsButton;
case "alwaysShow":
return this._labelAlwaysShow;
case "temperatureUnit":
return this._labelTemperatureUnit;
case "lightEntityId":
return this._labelLightEntityId;
case "powerEntityId":
return this._labelPowerEntityId;
case "cameraEntityId":
return this._labelCameraEntityId;
case "scaleFactor":
return this._labelScaleFactor;
case "slotColors":
return this._labelSlotColors;
default:
return this._labelPrinter_id;
}
};
private _computeSchemaMain(): HaFormBaseSchema[] {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!this.printers) {
return [];
}
const printerOptions = Object.keys(this.printers).map(
(printerID, _index) => ({
value: printerID,
label: this.printers[printerID].name,
}),
);
return [
{
name: "printer_id",
selector: {
select: {
options: printerOptions,
mode: "dropdown",
multiple: false,
},
},
},
{
name: "vertical",
selector: { boolean: {} },
},
{
name: "round",
selector: { boolean: {} },
},
{
name: "use_24hr",
selector: { boolean: {} },
},
{
name: "temperatureUnit",
selector: {
select: {
options: [
{
value: TemperatureUnit.C,
label: `°${TemperatureUnit.C}`,
},
{
value: TemperatureUnit.F,
label: `°${TemperatureUnit.F}`,
},
],
mode: "list",
multiple: false,
},
},
},
{
name: "alwaysShow",
selector: { boolean: {} },
},
{
name: "showSettingsButton",
selector: { boolean: {} },
},
{
name: "scaleFactor",
selector: {
select: {
options: [
{
value: 1,
label: "1",
},
{
value: 0.75,
label: "0.75",
},
{
value: 0.5,
label: "0.5",
},
],
mode: "list",
multiple: false,
},
},
},
{
name: "lightEntityId",
selector: { entity: { domain: LIGHT_ENTITY_DOMAINS } },
},
{
name: "powerEntityId",
selector: { entity: { domain: SWITCH_ENTITY_DOMAINS } },
},
{
name: "cameraEntityId",
selector: { entity: { domain: CAMERA_ENTITY_DOMAINS } },
},
];
}
private _computeSchemaColours(): HaFormBaseSchema[] {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return this.printers
? [
{
name: "slotColors",
selector: {
text: {
multiple: true,
},
},
},
]
: [];
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
.header {
color: var(--primary-text-color);
}
ha-tabs {
margin-left: max(env(safe-area-inset-left), 24px);
margin-right: max(env(safe-area-inset-right), 24px);
--paper-tabs-selection-bar-color: var(--primary-color);
text-transform: uppercase;
}
.ac-printer-card-configure-conf {
margin-top: 10px;
}
.ac-cconf-label {
margin-bottom: 4px;
font-weight: bold;
font-size: 14px;
}
`;
}
}

View File

@@ -1,464 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { animate, Options as motionOptions } from "@lit-labs/motion";
import { localize } from "../../../../localize/localize";
import { HASSDomEvent } from "../../../fire_event";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import {
getPrinterDryingButtonStateObj,
getPrinterEntityId,
isPrinterButtonStateAvailable,
} from "../../../helpers";
import {
AnycubicDryingPresetEntity,
HassDevice,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
ModalEventDrying,
} from "../../../types";
import { commonModalStyle } from "../../ui/modal-styles";
import "../../ui/select-dropdown.ts";
const animOptionsCard: motionOptions = {
keyframeOptions: {
duration: 250,
direction: "alternate",
easing: "ease-in-out",
},
properties: ["height", "opacity", "scale"],
};
const PRIMARY_DRYING_PRESET_1 = "drying_preset_1";
const PRIMARY_DRYING_PRESET_2 = "drying_preset_2";
const PRIMARY_DRYING_PRESET_3 = "drying_preset_3";
const PRIMARY_DRYING_PRESET_4 = "drying_preset_4";
const PRIMARY_DRYING_STOP = "drying_stop";
const SECONDARY_PREFIX = "secondary_";
const SECONDARY_DRYING_PRESET_1 = SECONDARY_PREFIX + PRIMARY_DRYING_PRESET_1;
const SECONDARY_DRYING_PRESET_2 = SECONDARY_PREFIX + PRIMARY_DRYING_PRESET_2;
const SECONDARY_DRYING_PRESET_3 = SECONDARY_PREFIX + PRIMARY_DRYING_PRESET_3;
const SECONDARY_DRYING_PRESET_4 = SECONDARY_PREFIX + PRIMARY_DRYING_PRESET_4;
const SECONDARY_DRYING_STOP = SECONDARY_PREFIX + PRIMARY_DRYING_STOP;
@customElementIfUndef("anycubic-printercard-multicolorbox_modal_drying")
export class AnycubicPrintercardMulticolorboxModalDrying extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@state()
private box_id: number = 0;
@state()
private _dryingPresetId1: string = PRIMARY_DRYING_PRESET_1;
@state()
private _dryingPresetId2: string = PRIMARY_DRYING_PRESET_2;
@state()
private _dryingPresetId3: string = PRIMARY_DRYING_PRESET_3;
@state()
private _dryingPresetId4: string = PRIMARY_DRYING_PRESET_4;
@state()
private _dryingStopId: string = PRIMARY_DRYING_STOP;
@state()
private _hasDryingPreset1: boolean = false;
@state()
private _hasDryingPreset2: boolean = false;
@state()
private _hasDryingPreset3: boolean = false;
@state()
private _hasDryingPreset4: boolean = false;
@state()
private _hasDryingStop: boolean = false;
@state()
private _dryingPresetTemp1: string = "";
@state()
private _dryingPresetDur1: string = "";
@state()
private _dryingPresetTemp2: string = "";
@state()
private _dryingPresetDur2: string = "";
@state()
private _dryingPresetTemp3: string = "";
@state()
private _dryingPresetDur3: string = "";
@state()
private _dryingPresetTemp4: string = "";
@state()
private _dryingPresetDur4: string = "";
@state()
private _isOpen: boolean = false;
@state()
private _heading: string;
@state()
private _buttonTextPreset: string;
@state()
private _buttonTextMinutes: string;
@state()
private _buttonStopDrying: string;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this.addEventListener("click", (e) => {
this._closeModal(e);
});
}
public connectedCallback(): void {
super.connectedCallback();
this.parentElement?.addEventListener(
"ac-mcbdry-modal",
this._handleModalEvent,
);
}
public disconnectedCallback(): void {
this.parentElement?.removeEventListener(
"ac-mcbdry-modal",
this._handleModalEvent,
);
super.disconnectedCallback();
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._heading = localize("card.drying_settings.heading", this.language);
this._buttonTextPreset = localize(
"card.drying_settings.button_preset",
this.language,
);
this._buttonTextMinutes = localize(
"card.drying_settings.button_minutes",
this.language,
);
this._buttonStopDrying = localize(
"card.drying_settings.button_stop_drying",
this.language,
);
}
if (changedProperties.has("box_id")) {
if (this.box_id === 1) {
this._dryingPresetId1 = SECONDARY_DRYING_PRESET_1;
this._dryingPresetId2 = SECONDARY_DRYING_PRESET_2;
this._dryingPresetId3 = SECONDARY_DRYING_PRESET_3;
this._dryingPresetId4 = SECONDARY_DRYING_PRESET_4;
this._dryingStopId = SECONDARY_DRYING_STOP;
} else {
this._dryingPresetId1 = PRIMARY_DRYING_PRESET_1;
this._dryingPresetId2 = PRIMARY_DRYING_PRESET_2;
this._dryingPresetId3 = PRIMARY_DRYING_PRESET_3;
this._dryingPresetId4 = PRIMARY_DRYING_PRESET_4;
this._dryingStopId = PRIMARY_DRYING_STOP;
}
}
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterDevice")
) {
const dryingPresetState1: AnycubicDryingPresetEntity =
getPrinterDryingButtonStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._dryingPresetId1,
) as AnycubicDryingPresetEntity;
this._hasDryingPreset1 =
isPrinterButtonStateAvailable(dryingPresetState1);
this._dryingPresetTemp1 = String(
dryingPresetState1.attributes.temperature,
);
this._dryingPresetDur1 = String(dryingPresetState1.attributes.duration);
const dryingPresetState2: AnycubicDryingPresetEntity =
getPrinterDryingButtonStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._dryingPresetId2,
) as AnycubicDryingPresetEntity;
this._hasDryingPreset2 =
isPrinterButtonStateAvailable(dryingPresetState2);
this._dryingPresetTemp2 = String(
dryingPresetState2.attributes.temperature,
);
this._dryingPresetDur2 = String(dryingPresetState2.attributes.duration);
const dryingPresetState3: AnycubicDryingPresetEntity =
getPrinterDryingButtonStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._dryingPresetId3,
) as AnycubicDryingPresetEntity;
this._hasDryingPreset3 =
isPrinterButtonStateAvailable(dryingPresetState3);
this._dryingPresetTemp3 = String(
dryingPresetState3.attributes.temperature,
);
this._dryingPresetDur3 = String(dryingPresetState3.attributes.duration);
const dryingPresetState4: AnycubicDryingPresetEntity =
getPrinterDryingButtonStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._dryingPresetId4,
) as AnycubicDryingPresetEntity;
this._hasDryingPreset4 =
isPrinterButtonStateAvailable(dryingPresetState4);
this._dryingPresetTemp4 = String(
dryingPresetState4.attributes.temperature,
);
this._dryingPresetDur4 = String(dryingPresetState4.attributes.duration);
const dryingStopState = getPrinterDryingButtonStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._dryingStopId,
);
this._hasDryingStop = isPrinterButtonStateAvailable(dryingStopState);
}
}
protected update(changedProperties: PropertyValues<this>): void {
super.update(changedProperties);
if (this._isOpen) {
this.style.display = "block";
} else {
this.style.display = "none";
}
}
render(): LitTemplateResult {
const stylesMain = {
height: "auto",
opacity: 1.0,
scale: 1.0,
};
return html`
<div
class="ac-modal-container"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<span class="ac-modal-close" @click=${this._closeModal}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this._renderCard()}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return html`
<div>
<div class="ac-drying-header">${this._heading}</div>
<div class="ac-drying-buttonscont">
${this._hasDryingPreset1
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset1}>
${this._buttonTextPreset} 1<br />
${this._dryingPresetDur1} ${this._buttonTextMinutes} @
${this._dryingPresetTemp1}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset2
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset2}>
${this._buttonTextPreset} 2<br />
${this._dryingPresetDur2} ${this._buttonTextMinutes} @
${this._dryingPresetTemp2}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset3
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset3}>
${this._buttonTextPreset} 3<br />
${this._dryingPresetDur3} ${this._buttonTextMinutes} @
${this._dryingPresetTemp3}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset4
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset4}>
${this._buttonTextPreset} 4<br />
${this._dryingPresetDur4} ${this._buttonTextMinutes} @
${this._dryingPresetTemp4}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingStop
? html`
<div class="ac-flex-break"></div>
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingStop}>
${this._buttonStopDrying}
</ha-control-button>
</div>
`
: nothing}
</div>
</div>
`;
}
private _pressHassButton(suffix: string): void {
if (this.printerEntityIdPart) {
this.hass
.callService("button", "press", {
entity_id: getPrinterEntityId(
this.printerEntityIdPart,
"button",
suffix,
),
})
.then()
.catch((_e: unknown) => {
// Show in error modal
});
}
}
private _handleDryingPreset1 = (): void => {
this._pressHassButton(this._dryingPresetId1);
this._closeModal();
};
private _handleDryingPreset2 = (): void => {
this._pressHassButton(this._dryingPresetId2);
this._closeModal();
};
private _handleDryingPreset3 = (): void => {
this._pressHassButton(this._dryingPresetId3);
this._closeModal();
};
private _handleDryingPreset4 = (): void => {
this._pressHassButton(this._dryingPresetId4);
this._closeModal();
};
private _handleDryingStop = (): void => {
this._pressHassButton(this._dryingStopId);
this._closeModal();
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventDrying>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this.box_id = Number(e.detail.box_id);
}
};
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this.box_id = 0;
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-drying-header {
font-size: 24px;
text-align: center;
font-weight: 600;
}
ha-control-button {
min-width: 150px;
font-size: 14px;
min-height: 55px;
width: 100%;
box-sizing: border-box;
}
.ac-flex-break {
flex-basis: 100%;
height: 0;
}
.ac-drying-buttonscont {
display: flex;
flex-wrap: wrap;
margin-top: 30px;
align-items: center;
justify-content: center;
}
.ac-drying-buttoncont {
width: 50%;
margin: 0;
position: relative;
box-sizing: border-box;
padding: 10px;
}
`;
}
}

View File

@@ -1,377 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { query } from "lit/decorators/query.js";
import { map } from "lit/directives/map.js";
import { styleMap } from "lit/directives/style-map.js";
import { animate, Options as motionOptions } from "@lit-labs/motion";
import { localize } from "../../../../localize/localize";
import "../../../lib/colorpicker/ColorPicker.js";
import { platform } from "../../../const";
import { HASSDomEvent } from "../../../fire_event";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { materialTypeFromString } from "../../../helpers";
import {
AnycubicMaterialType,
AnycubicSpoolInfo,
ColorPicker,
ColourPickEvent,
DomClickEvent,
DropdownEvent,
EvtTargColourPreset,
HassDevice,
HomeAssistant,
LitTemplateResult,
ModalEventSpool,
} from "../../../types";
import { commonModalStyle } from "../../ui/modal-styles";
import "../../ui/select-dropdown.ts";
const animOptionsCard: motionOptions = {
keyframeOptions: {
duration: 250,
direction: "alternate",
easing: "ease-in-out",
},
properties: ["height", "opacity", "scale"],
};
@customElementIfUndef("anycubic-printercard-multicolorbox_modal_spool")
export class AnycubicPrintercardMulticolorboxModalSpool extends LitElement {
@query("color-picker")
private _elColorPicker: ColorPicker | undefined;
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@property({ attribute: "slot-colors" })
public slotColors?: string[];
@state()
private box_id: number = 0;
@state()
private spoolList: AnycubicSpoolInfo[] = [];
@state()
private spool_index: number = -1;
@state()
private material_type: AnycubicMaterialType | undefined;
@state()
private color: number[] | string | undefined;
@state()
private _isOpen: boolean = false;
@state()
private _heading: string;
@state()
private _labelSelectMaterial: string;
@state()
private _labelSelectColour: string;
@state()
private _buttonSave: string;
@state()
private _changingSlot: boolean = false;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this.addEventListener("click", (e) => {
this._closeModal(e);
});
this.addEventListener("ac-select-dropdown", this._handleDropdownEvent);
this.addEventListener("colorchanged", this._handleColourEvent);
this.addEventListener("colorpicked", this._handleColourPickEvent);
}
public connectedCallback(): void {
super.connectedCallback();
this.parentElement?.addEventListener(
"ac-mcb-modal",
this._handleModalEvent,
);
}
public disconnectedCallback(): void {
this.parentElement?.removeEventListener(
"ac-mcb-modal",
this._handleModalEvent,
);
super.disconnectedCallback();
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._heading = localize("card.spool_settings.heading", this.language);
this._labelSelectMaterial = localize(
"card.spool_settings.label_select_material",
this.language,
);
this._labelSelectColour = localize(
"card.spool_settings.label_select_colour",
this.language,
);
this._buttonSave = localize("common.actions.save", this.language);
}
}
protected update(changedProperties: PropertyValues<this>): void {
super.update(changedProperties);
if (this._isOpen) {
this.style.display = "block";
} else {
this.style.display = "none";
}
}
render(): LitTemplateResult {
const stylesMain = {
height: "auto",
opacity: 1.0,
scale: 1.0,
};
return html`
<div
class="ac-modal-container"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<span class="ac-modal-close" @click=${this._closeModal}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this.color ? this._renderCard() : nothing}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return this.spool_index >= 0
? html`
<div>
<div class="ac-slot-title">
${this._heading}: ${this.spool_index + 1}
</div>
<div>
<div>
<p class="ac-modal-label">${this._labelSelectMaterial}:</p>
<anycubic-ui-select-dropdown
.availableOptions=${AnycubicMaterialType}
.placeholder=${AnycubicMaterialType.PLA}
.initialItem=${this.material_type}
></anycubic-ui-select-dropdown>
</div>
${this._renderPresets()}
<div>
<p class="ac-modal-label">${this._labelSelectColour}:</p>
<color-picker .value=${this.color}></color-picker>
</div>
</div>
<div class="ac-save-settings">
<ha-control-button
.disabled=${this._changingSlot}
@click=${this._handleSaveButton}
>
${this._buttonSave}
</ha-control-button>
</div>
</div>
`
: nothing;
}
private _renderPresets(): LitTemplateResult {
return html`
<div>
<p class="ac-modal-label">Choose Preset Colour:</p>
<div class="ac-mcb-presets">
${this.slotColors
? map(this.slotColors, (preset, _index) => {
const presetStyle = {
"background-color": preset,
};
return html`
<div
class="ac-mcb-preset-color"
style=${styleMap(presetStyle)}
.preset=${preset}
@click=${this._colourPresetChange}
>
&nbsp;
</div>
`;
})
: nothing}
</div>
</div>
`;
}
private _colourPresetChange = (
ev: DomClickEvent<EvtTargColourPreset>,
): void => {
this.color = ev.currentTarget.preset;
if (this._elColorPicker) {
this._elColorPicker.color = this.color;
}
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventSpool>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this.box_id = Number(e.detail.box_id);
this.spool_index = Number(e.detail.spool_index);
this.material_type = materialTypeFromString(e.detail.material_type);
this.color = e.detail.color;
}
};
private _handleDropdownEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<DropdownEvent<string, string>>;
e.stopPropagation();
if (e.detail.value) {
this.material_type = materialTypeFromString(e.detail.value);
}
};
private _handleColourEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ColourPickEvent>;
e.stopPropagation();
if (e.detail.color) {
this.color = e.detail.color.rgb;
}
};
private _handleColourPickEvent = (e: Event): void => {
this._handleColourEvent(e);
if (!this._changingSlot) {
this._submitSlotChanges();
}
};
private _handleSaveButton = (): void => {
this._submitSlotChanges();
};
private _serviceAvailable(serviceName: string): boolean {
return Boolean(this.hass?.services?.[platform]?.[serviceName]);
}
private _submitSlotChanges(): void {
if (
this.selectedPrinterDevice &&
this.material_type &&
this.spool_index >= 0 &&
this.color &&
this.color.length >= 3
) {
const serv = `multi_color_box_set_slot_${this.material_type.toLowerCase()}`;
if (!this._serviceAvailable(serv)) {
this._closeModal();
return;
}
this._changingSlot = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
box_id: this.box_id,
slot_number: this.spool_index + 1,
slot_color_red: this.color[0],
slot_color_green: this.color[1],
slot_color_blue: this.color[2],
})
.then(() => {
this._changingSlot = false;
})
.catch((_e: unknown) => {
this._changingSlot = false;
});
this._closeModal();
}
}
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this.spool_index = -1;
this.material_type = undefined;
this.color = undefined;
this.box_id = 0;
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-slot-title {
font-size: 24px;
text-align: center;
font-weight: 600;
}
.ac-mcb-presets {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.ac-mcb-preset-color {
width: 30px;
height: 30px;
border-radius: 15px;
margin: 20px 10px;
}
ha-control-button {
min-width: 150px;
margin: 30px auto 0px;
font-size: 14px;
}
color-picker {
--font-fam: var(--token-font-family-primary);
--bg-color: var(--ha-card-background);
--label-color: var(--secondary-text-color);
--form-border-color: var(--ha-card-background);
--input-active-border-color: var(--primary-color);
--input-bg: var(--primary-background-color);
--input-active-bg: var(--ha-card-background);
--input-color: var(--secondary-text-color);
--input-active-color: var(--primary-text-color);
--input-active-box-shadow: 0 2px 5px #ccc;
--button-active-bg: var(--state-active-color);
--button-active-color: var(--token-color-icon-primary);
--outer-box-shadow: 0 4px 12px #111;
}
`;
}
}

View File

@@ -1,361 +0,0 @@
import { mdiRadiator } from "@mdi/js";
import { CSSResult, LitElement, PropertyValues, css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { styleMap } from "lit/directives/style-map.js";
import { localize } from "../../../../localize/localize";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { fireEvent } from "../../../fire_event";
import {
getPrinterEntityId,
getPrinterSensorStateObj,
getPrinterSwitchStateObj,
} from "../../../helpers";
import {
AnycubicSpoolInfo,
AnycubicSpoolInfoEntity,
DomClickEvent,
EvtTargSpoolEdit,
HassEntity,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
} from "../../../types";
const SECONDARY_PREFIX = "secondary_";
const PRIMARY_ENTITY_ID_RUNOUT_REFILL = "ace_run_out_refill";
const SECONDARY_ENTITY_ID_RUNOUT_REFILL =
SECONDARY_PREFIX + PRIMARY_ENTITY_ID_RUNOUT_REFILL;
const PRIMARY_ENTITY_ID_SPOOLS = "ace_spools";
const SECONDARY_ENTITY_ID_SPOOLS = SECONDARY_PREFIX + PRIMARY_ENTITY_ID_SPOOLS;
@customElementIfUndef("anycubic-printercard-multicolorbox_view")
export class AnycubicPrintercardMulticolorboxview extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@property()
public box_id: number = 0;
@state()
private _runoutRefillId: string = PRIMARY_ENTITY_ID_RUNOUT_REFILL;
@state()
private _spoolsEntityId: string = PRIMARY_ENTITY_ID_SPOOLS;
@state()
private spoolList: AnycubicSpoolInfo[] = [];
@state()
private selectedIndex: number = -1;
@state()
private selectedMaterialType: string = "";
@state()
private selectedColor: number[] = [0, 0, 0];
@state()
private _runoutRefillState: HassEntity | undefined;
@state()
private _buttonRefill: string;
@state()
private _buttonDry: string;
@state()
private _changingRunout: boolean = false;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._buttonRefill = localize(
"card.buttons.runout_refill",
this.language,
);
this._buttonDry = localize("card.buttons.dry", this.language);
}
if (changedProperties.has("box_id")) {
if (this.box_id === 1) {
this._runoutRefillId = SECONDARY_ENTITY_ID_RUNOUT_REFILL;
this._spoolsEntityId = SECONDARY_ENTITY_ID_SPOOLS;
} else {
this._runoutRefillId = PRIMARY_ENTITY_ID_RUNOUT_REFILL;
this._spoolsEntityId = PRIMARY_ENTITY_ID_SPOOLS;
}
}
if (
changedProperties.has("hass") ||
changedProperties.has("printerEntities") ||
changedProperties.has("printerEntityIdPart")
) {
this.spoolList = (
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._spoolsEntityId,
"not loaded",
{ spool_info: [] },
) as AnycubicSpoolInfoEntity
).attributes.spool_info;
this._runoutRefillState = getPrinterSwitchStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
this._runoutRefillId,
);
}
}
render(): LitTemplateResult {
return html`
<div class="ac-printercard-mcbview">
<div class="ac-printercard-mcbmenu ac-printercard-menuleft">
<div class="ac-switch" @click=${this._handleRunoutRefillChanged}>
<div class="ac-switch-label">${this._buttonRefill}</div>
<ha-entity-toggle
.hass=${this.hass}
.stateObj=${this._runoutRefillState}
></ha-entity-toggle>
</div>
</div>
<div class="ac-printercard-spoolcont">${this._renderSpools()}</div>
<div class="ac-printercard-mcbmenu ac-printercard-menuright">
<ha-control-button @click=${this._openDryingModal}>
<ha-svg-icon .path=${mdiRadiator}></ha-svg-icon>
${this._buttonDry}
</ha-control-button>
</div>
</div>
`;
}
private _renderSpools(): Generator<
AnycubicSpoolInfo,
void,
LitTemplateResult
> {
return map(
this.spoolList,
(spool: AnycubicSpoolInfo, index: number): LitTemplateResult => {
const ringStyle = {
"background-color": spool.spool_loaded
? `rgb(${spool.color[0]}, ${spool.color[1]}, ${spool.color[2]})`
: "#aaa",
};
return html`
<div
class="ac-spool-info"
.index=${index}
.material_type=${spool.material_type}
.color=${spool.color}
@click=${this._editSpool}
>
<div class="ac-spool-color-ring-cont">
<div
class="ac-spool-color-ring-inner"
style=${styleMap(ringStyle)}
>
<div class="ac-spool-color-num">${index + 1}</div>
</div>
</div>
<div class="ac-spool-material-type">
${spool.spool_loaded ? spool.material_type : "---"}
</div>
</div>
`;
},
) as Generator<AnycubicSpoolInfo, void, LitTemplateResult>;
}
private _openDryingModal = (): void => {
fireEvent(this, "ac-mcbdry-modal", {
modalOpen: true,
box_id: this.box_id,
});
};
private _handleRunoutRefillChanged = (_ev: Event): void => {
// const refillActive = ev.target.checked;
if (this._changingRunout) {
return;
}
this._changingRunout = true;
this.hass
.callService("switch", "toggle", {
entity_id: getPrinterEntityId(
this.printerEntityIdPart,
"switch",
this._runoutRefillId,
),
})
.then(() => {
this._changingRunout = false;
})
.catch((_e: unknown) => {
this._changingRunout = false;
});
};
private _editSpool = (ev: DomClickEvent<EvtTargSpoolEdit>): void => {
const index: number = ev.currentTarget.index;
const material_type: string = ev.currentTarget.material_type;
const color: number[] = ev.currentTarget.color;
fireEvent(this, "ac-mcb-modal", {
modalOpen: true,
box_id: this.box_id,
spool_index: index,
material_type: material_type,
color: color,
});
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-printercard-mcbview {
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
box-sizing: border-box;
width: 100%;
}
.ac-printercard-mcbmenu {
height: 100%;
position: relative;
width: 10.42%;
}
.ac-printercard-spoolcont {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 62.5%;
}
.ac-spool-info {
box-sizing: border-box;
height: auto;
cursor: pointer;
width: 25%;
padding: 5px;
}
.ac-spool-color-ring-cont {
position: relative;
width: 100%;
box-sizing: border-box;
}
.ac-spool-color-ring-cont:before {
content: "";
display: block;
padding-top: 100%;
}
.ac-spool-color-ring-inner {
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
background-color: #aaa;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.ac-spool-color-num {
font-weight: 900;
box-sizing: border-box;
border-radius: 50%;
background-color: #eee;
width: 46.5%;
height: 46.5%;
color: #222;
text-align: center;
}
.ac-spool-color-num:before {
content: "";
display: inline-block;
height: 100%;
vertical-align: middle;
padding-top: 2.5px;
}
.ac-spool-material-type {
height: auto;
text-align: center;
font-weight: 900;
}
.ac-printercard-mcbmenu ha-control-button {
font-size: 12px;
margin: 0px;
position: absolute;
top: 50%;
transform: translateY(-50%);
min-width: 48px;
min-height: 48px;
width: 100%;
}
.ac-printercard-menuright ha-control-button {
right: 0px;
}
.ac-printercard-mcbmenu .ac-switch-label {
font-size: 12px;
}
.ac-printercard-mcbmenu .ac-switch {
display: flex;
flex-wrap: wrap;
text-align: center;
margin: 0px;
position: absolute;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
box-sizing: border-box;
padding: 4px 4px;
justify-content: center;
background-color: #8686862e;
border-radius: 8px;
}
.ac-printercard-mcbmenu .ac-switch:hover {
background-color: #86868669;
}
`;
}
}

View File

@@ -1,279 +0,0 @@
import { LitElement, PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import * as pkgjson from "../../../package.json";
import {
AnycubicCardConfig,
CustomCardsWindow,
HassDevice,
HassDeviceList,
HomeAssistant,
LitTemplateResult,
PrinterCardStatType,
TemperatureUnit,
} from "../../types";
import {
getDefaultCardConfig,
getPrinterDevices,
getSelectedPrinter,
undefinedDefault,
} from "../../helpers";
import "./card/card.ts";
import "./configure/configure.ts";
window.console.info(
`%c KOBRAX-LAN-CARD %c v${pkgjson.version} `,
"color: orange; font-weight: bold; background: black",
"color: white; font-weight: bold; background: dimgray",
);
const defaultConfig = getDefaultCardConfig();
@customElement("kobrax-lan-card-editor")
export class AnycubicPrintercardEditor extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public config: AnycubicCardConfig = {};
@state()
private printers?: HassDeviceList;
@state()
private language: string;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this.printers = getPrinterDevices(this.hass);
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("hass") && this.hass.language !== this.language) {
this.language = this.hass.language;
}
if (changedProperties.has("config")) {
this.config.vertical = undefinedDefault(
this.config.vertical,
defaultConfig.vertical,
) as boolean;
this.config.round = undefinedDefault(
this.config.round,
defaultConfig.round,
) as boolean;
this.config.use_24hr = undefinedDefault(
this.config.use_24hr,
defaultConfig.use_24hr,
) as boolean;
this.config.alwaysShow = undefinedDefault(
this.config.alwaysShow,
defaultConfig.alwaysShow,
) as boolean;
this.config.showSettingsButton = undefinedDefault(
this.config.showSettingsButton,
defaultConfig.showSettingsButton,
) as boolean;
this.config.temperatureUnit = undefinedDefault(
this.config.temperatureUnit,
defaultConfig.temperatureUnit,
) as TemperatureUnit;
this.config.monitoredStats = undefinedDefault(
this.config.monitoredStats,
defaultConfig.monitoredStats,
) as PrinterCardStatType[];
this.config.slotColors = undefinedDefault(
this.config.slotColors,
defaultConfig.slotColors,
) as string[];
this.config.scaleFactor = undefinedDefault(
this.config.scaleFactor,
defaultConfig.scaleFactor,
) as number;
}
}
public setConfig(config: AnycubicCardConfig): void {
this.config = config;
}
render(): LitTemplateResult {
return html`
<anycubic-printercard-configure
.hass=${this.hass}
.language=${this.language}
.printers=${this.printers}
.cardConfig=${this.config}
></anycubic-printercard-configure>
`;
}
}
@customElement("kobrax-lan-card")
export class AnycubicCard extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public config: AnycubicCardConfig = {};
@state()
private printers?: HassDeviceList;
@state()
private language: string;
@state()
private selectedPrinterID: string | undefined;
@state()
private selectedPrinterDevice: HassDevice | undefined;
@state()
private vertical?: boolean;
@state()
private round?: boolean;
@state()
private use_24hr?: boolean;
@state()
private showSettingsButton?: boolean;
@state()
private alwaysShow?: boolean;
@state()
private temperatureUnit: TemperatureUnit | undefined;
@state()
private lightEntityId?: string | undefined;
@state()
private powerEntityId?: string | undefined;
@state()
private cameraEntityId?: string | undefined;
@state()
private scaleFactor?: number | undefined;
@state()
private slotColors?: string[];
@state()
private monitoredStats: PrinterCardStatType[] | undefined;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this.printers = getPrinterDevices(this.hass);
this.requestUpdate();
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("hass") && this.hass.language !== this.language) {
this.language = this.hass.language;
}
if (changedProperties.has("config") || changedProperties.has("printers")) {
this.vertical = undefinedDefault(
this.config.vertical,
defaultConfig.vertical,
) as boolean;
this.round = undefinedDefault(
this.config.round,
defaultConfig.round,
) as boolean;
this.use_24hr = undefinedDefault(
this.config.use_24hr,
defaultConfig.use_24hr,
) as boolean;
this.alwaysShow = undefinedDefault(
this.config.alwaysShow,
defaultConfig.alwaysShow,
) as boolean;
this.showSettingsButton = undefinedDefault(
this.config.showSettingsButton,
defaultConfig.showSettingsButton,
) as boolean;
this.temperatureUnit = undefinedDefault(
this.config.temperatureUnit,
defaultConfig.temperatureUnit,
) as TemperatureUnit;
this.lightEntityId = this.config.lightEntityId;
this.powerEntityId = this.config.powerEntityId;
this.cameraEntityId = this.config.cameraEntityId;
this.scaleFactor = this.config.scaleFactor;
this.slotColors = this.config.slotColors;
this.monitoredStats = this.config.monitoredStats;
if (this.config.printer_id && this.printers) {
this.selectedPrinterID = this.config.printer_id;
this.selectedPrinterDevice = getSelectedPrinter(
this.printers,
this.config.printer_id,
);
}
}
}
public setConfig(config: AnycubicCardConfig): void {
this.config = config;
}
render(): LitTemplateResult {
return html`
<anycubic-printercard-card
.hass=${this.hass}
.language=${this.language}
.monitoredStats=${this.config.monitoredStats}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
.vertical=${this.vertical}
.round=${this.round}
.use_24hr=${this.use_24hr}
.showSettingsButton=${this.showSettingsButton}
.alwaysShow=${this.alwaysShow}
.temperatureUnit=${this.temperatureUnit}
.lightEntityId=${this.lightEntityId}
.powerEntityId=${this.powerEntityId}
.cameraEntityId=${this.cameraEntityId}
.scaleFactor=${this.scaleFactor}
.slotColors=${this.slotColors}
></anycubic-printercard-card>
`;
}
public getCardSize(): number {
return 2;
}
static getConfigElement(): HTMLElement {
return document.createElement("kobrax-lan-card-editor");
}
static getStubConfig(
hass: HomeAssistant,
_entities: string[],
_entitiesFallback: string[],
): AnycubicCardConfig {
return { printer_id: Object.keys(getPrinterDevices(hass))[0] };
}
}
const customCardsWindow = window as CustomCardsWindow;
customCardsWindow.customCards = customCardsWindow.customCards || [];
customCardsWindow.customCards.push({
type: "kobrax-lan-card",
name: "Kobrax LAN Card",
preview: true,
description: "Kobrax LAN Integration Card",
});

View File

@@ -1,396 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { query } from "lit/decorators/query.js";
import { ResizeController } from "@lit-labs/observers/resize-controller.js";
import { animate, Options as motionOptions } from "@lit-labs/motion";
import { getDimensions } from "./utils";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import {
getPrinterImageStateUrl,
getPrinterSensorStateObj,
isPrintStatePrinting,
updateElementStyleWithObject,
} from "../../../helpers";
import {
AnimatedPrinterConfig,
AnimatedPrinterDimensions,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
} from "../../../types";
const animOptionsGantry: motionOptions = {
keyframeOptions: {
duration: 2000,
direction: "alternate",
composite: "add",
},
properties: ["left"],
};
const animOptionsAxis: motionOptions = {
keyframeOptions: {
duration: 100,
composite: "add",
},
properties: ["top"],
};
@customElementIfUndef("anycubic-printercard-animated_printer")
export class AnycubicPrintercardAnimatedPrinter extends LitElement {
@query(".ac-printercard-animatedprinter")
private _rootElement: HTMLElement | undefined;
@query(".ac-apr-scalable")
private _elAcAPr_scalable: HTMLElement | undefined;
@query(".ac-apr-frame")
private _elAcAPr_frame: HTMLElement | undefined;
@query(".ac-apr-hole")
private _elAcAPr_hole: HTMLElement | undefined;
@query(".ac-apr-buildarea")
private _elAcAPr_buildarea: HTMLElement | undefined;
@query(".ac-apr-animprint")
private _elAcAPr_animprint: HTMLElement | undefined;
@query(".ac-apr-buildplate")
private _elAcAPr_buildplate: HTMLElement | undefined;
@query(".ac-apr-xaxis")
private _elAcAPr_xaxis: HTMLElement | undefined;
@query(".ac-apr-gantry")
private _elAcAPr_gantry: HTMLElement | undefined;
@query(".ac-apr-nozzle")
private _elAcAPr_nozzle: HTMLElement | undefined;
@property()
public hass!: HomeAssistant;
@property({ attribute: "scale-factor" })
public scaleFactor?: number;
@property({ attribute: "printer-config" })
public printerConfig: AnimatedPrinterConfig;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@state()
private dimensions: AnimatedPrinterDimensions | undefined;
@state()
private resizeObserver: ResizeController | undefined;
@state()
private _progressNum: number = 0;
@state()
private animKeyframeGantry: number = 0;
@state()
private _isPrinting: boolean = false;
@state()
private imagePreviewUrl: string | undefined;
@state()
private imagePreviewBgUrl: string | undefined;
public connectedCallback(): void {
super.connectedCallback();
this.resizeObserver = new ResizeController(this, {
callback: this._onResizeEvent,
});
if (this.dimensions && this._isPrinting) {
this._moveGantry();
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("scaleFactor")) {
this._onResizeEvent();
}
if (
changedProperties.has("hass") ||
changedProperties.has("printerEntities") ||
changedProperties.has("printerEntityIdPart")
) {
const prevUrl = getPrinterImageStateUrl(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_preview",
);
if (this.imagePreviewUrl !== prevUrl) {
this.imagePreviewUrl = prevUrl;
this.imagePreviewBgUrl = this.imagePreviewUrl
? `url('${prevUrl}')`
: undefined;
}
this._progressNum =
Number(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_progress",
0,
).state,
) / 100;
const printingState = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_state",
).state.toLowerCase();
const newIsPrinting = isPrintStatePrinting(printingState);
if (this.dimensions && !this._isPrinting && newIsPrinting) {
this._moveGantry();
}
this._isPrinting = newIsPrinting;
}
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (
(changedProperties.has("dimensions") ||
changedProperties.has("animKeyframeGantry") ||
changedProperties.has("hass")) &&
this.dimensions
) {
const progY = this._progressNum * -1 * this.dimensions.BuildArea.height;
updateElementStyleWithObject(this._elAcAPr_xaxis, {
...this.dimensions.XAxis,
top: this.dimensions.XAxis.top + progY,
});
updateElementStyleWithObject(this._elAcAPr_gantry, {
...this.dimensions.Gantry,
left:
this.animKeyframeGantry !== 0
? this.dimensions.Gantry.left + this.dimensions.BuildPlate.width
: this.dimensions.Gantry.left,
top: this.dimensions.Gantry.top + progY,
});
updateElementStyleWithObject(this._elAcAPr_animprint, {
height: `${this._progressNum * 100}%`,
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (changedProperties.has("dimensions") && this.dimensions) {
updateElementStyleWithObject(this._elAcAPr_scalable, {
...this.dimensions.Scalable,
});
updateElementStyleWithObject(this._elAcAPr_frame, {
...this.dimensions.Frame,
});
updateElementStyleWithObject(this._elAcAPr_hole, {
...this.dimensions.Hole,
});
updateElementStyleWithObject(this._elAcAPr_buildarea, {
...this.dimensions.BuildArea,
});
updateElementStyleWithObject(this._elAcAPr_buildplate, {
...this.dimensions.BuildPlate,
});
updateElementStyleWithObject(this._elAcAPr_nozzle, {
...this.dimensions.Nozzle,
});
}
}
}
render(): LitTemplateResult {
const stylesPreview = {
"background-image": this.imagePreviewBgUrl,
};
return html`
<div class="ac-printercard-animatedprinter">
${this.dimensions
? html` <div class="ac-apr-scalable">
<div class="ac-apr-frame">
<div class="ac-apr-hole"></div>
</div>
<div class="ac-apr-buildarea">
<div class="ac-apr-animprint">
${this.imagePreviewBgUrl
? html`
<div
class="ac-apr-imgprev"
style=${styleMap(stylesPreview)}
></div>
`
: nothing}
</div>
</div>
<div class="ac-apr-buildplate"></div>
<div
class="ac-apr-xaxis"
${animate({ ...animOptionsAxis })}
></div>
<div
class="ac-apr-gantry"
${animate({ ...animOptionsAxis })}
${animate(this._gantryAnimOptions)}
>
<div class="ac-apr-nozzle"></div>
</div>
</div>`
: nothing}
</div>
`;
}
private _gantryAnimOptions = (): motionOptions => {
return {
...animOptionsGantry,
onComplete: this._moveGantry,
disabled: !(this.dimensions && this._isPrinting),
};
};
private _onResizeEvent = (): void => {
if (this._rootElement) {
const height: number = this._rootElement.clientHeight;
const width: number = this._rootElement.clientWidth;
this._setDimensions(width, height);
}
};
private _setDimensions(width: number, height: number): void {
this.dimensions = getDimensions(
this.printerConfig,
{ width, height },
this.scaleFactor || 1.0,
);
}
private _moveGantry = (): void => {
this.animKeyframeGantry = this._isPrinting
? Number(!this.animKeyframeGantry)
: 0;
};
static get styles(): CSSResult {
return css`
:host {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.ac-printercard-animatedprinter {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.ac-apr-scalable {
position: relative;
}
.ac-apr-frame {
top: 0px;
left: 0px;
border-radius: 8px;
background-color: #bbbbbb;
position: absolute;
}
.ac-apr-hole {
position: absolute;
top: 0px;
left: 0px;
background-color: var(
--ha-card-background,
var(--card-background-color, white)
);
border-radius: 8px;
}
.ac-apr-buildarea {
background-color: rgba(0, 0, 0, 0.075);
box-sizing: border-box;
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
border-radius: 8px;
overflow: hidden;
}
.ac-apr-buildplate {
box-sizing: border-box;
border-radius: 8px;
position: absolute;
background-color: #333333;
height: 8px;
}
.ac-apr-xaxis {
position: absolute;
border-radius: 8px;
background-color: #aaaaaa;
}
.ac-apr-animprint {
background-color: var(--primary-text-color);
width: 100%;
}
.ac-apr-imgprev {
height: 100%;
width: 100%;
background-size: 100%;
background-repeat: no-repeat;
background-position-y: 100%;
}
.ac-apr-gantry {
background-color: #333333;
border-radius: 4px;
box-sizing: border-box;
position: absolute;
}
.ac-apr-nozzle {
background-color: #aaaaaa;
position: absolute;
width: 12px;
height: 12px;
clip-path: polygon(100% 0, 100% 50%, 50% 75%, 0 50%, 0 0);
}
`;
}
}

View File

@@ -1,65 +0,0 @@
import { CSSResult, LitElement, css, html } from "lit";
import { property } from "lit/decorators.js";
import { printerConfigAnycubic } from "./utils";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import {
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
} from "../../../types";
import "./animated_printer.ts";
@customElementIfUndef("anycubic-printercard-printer_view")
export class AnycubicPrintercardPrinterview extends LitElement {
@property()
public hass!: HomeAssistant;
@property({ attribute: "toggle-video", type: Function })
public toggleVideo?: () => void;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@property({ attribute: "scale-factor" })
public scaleFactor?: number;
render(): LitTemplateResult {
return html`
<div class="ac-printercard-printerview" @click=${this._viewClick}>
<anycubic-printercard-animated_printer
.hass=${this.hass}
.scaleFactor=${this.scaleFactor}
.printerEntities=${this.printerEntities}
.printerEntityIdPart=${this.printerEntityIdPart}
.printerConfig=${printerConfigAnycubic}
></anycubic-printercard-animated_printer>
</div>
`;
}
private _viewClick = (): void => {
if (this.toggleVideo) {
this.toggleVideo();
}
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-printercard-printerview {
height: 100%;
box-sizing: border-box;
}
`;
}
}

View File

@@ -1,199 +0,0 @@
import {
AnimatedPrinterBasicDimension,
AnimatedPrinterConfig,
AnimatedPrinterDimensions,
} from "../../../types";
class Scale {
scale_factor: number;
constructor(scale_factor: number) {
this.scale_factor = scale_factor;
}
val(value): number {
return this.scale_factor * value;
}
og(value): number {
return value / this.scale_factor;
}
scaleFactor(): number {
return this.scale_factor;
}
}
export function getDimensions(
config: AnimatedPrinterConfig,
bounds: AnimatedPrinterBasicDimension,
haScaleFactor: number,
): AnimatedPrinterDimensions {
/* We estimate the initial scale factor based on the height + width of the frame, then compound with set factor */
const scaledBoundsHeight =
bounds.height /
(config.top.height + config.bottom.height + config.left.height);
const scaledBoundsWidth =
bounds.width / (config.top.width + config.left.width + config.right.width);
const scale = new Scale(
Math.min(scaledBoundsHeight, scaledBoundsWidth) * haScaleFactor,
);
/* Frame */
const F_W = scale.val(config.top.width); // Width
const F_H = scale.val(
config.top.height + config.bottom.height + config.left.height,
); // Height
/* Scalable */
// const S_ML = (bounds.width - F_W) / 2; // Margin Left
// const S_MT = (bounds.height - F_H) / 2; // Margin Top
/* Hole */
const H_W = scale.val(
config.top.width - (config.left.width + config.right.width),
); // Width
const H_H = scale.val(config.left.height); // Height
const H_L = scale.val(config.left.width); // Left
const H_T = scale.val(config.top.height); // Top
/* Basis */
const BASIS_Y =
scale.val(config.top.height - config.buildplate.verticalOffset) + H_H;
const BASIS_X =
BASIS_Y +
scale.val(
(config.xAxis.extruder.height - config.xAxis.height) / 2 -
(config.xAxis.extruder.height + 12),
);
/* Build Area */
const B_W = scale.val(config.buildplate.maxWidth); // Width
const B_H = scale.val(config.buildplate.maxHeight); // Height
const B_L = scale.val(
config.left.width + (scale.og(H_W) - config.buildplate.maxWidth) / 2,
); // Left
const B_T = BASIS_Y - scale.val(config.buildplate.maxHeight); // Top
/* Build Plate */
const P_W = B_W; // Width
const P_L = B_L; // Left
const P_T = BASIS_Y; // Top
/* X Axis */
const X_W = scale.val(config.xAxis.width);
const X_H = scale.val(config.xAxis.height);
const X_L = scale.val(config.xAxis.offsetLeft);
/* Track */
const T_W = X_W;
const T_H = X_H;
/* Extruder */
const E_W = scale.val(config.xAxis.extruder.width);
const E_H = scale.val(config.xAxis.extruder.height);
const E_L = P_L - E_W / 2;
const E_M = E_L + B_W;
/* Nozzle */
const N_W = scale.val(12);
const N_H = scale.val(12);
const N_L = (E_W - N_W) / 2;
const N_T = E_H;
const E_T = P_T - E_H - N_H;
const X_T = E_T + E_H * 0.7 - X_H / 2;
return {
Scalable: {
width: F_W,
height: F_H,
},
Frame: {
width: F_W,
height: F_H,
},
Hole: {
width: H_W,
height: H_H,
left: H_L,
top: H_T,
},
BuildArea: {
width: B_W,
height: B_H,
left: B_L,
top: B_T,
},
BuildPlate: {
width: P_W,
left: P_L,
top: P_T,
},
XAxis: {
width: X_W,
height: X_H,
left: X_L,
top: X_T,
},
Track: {
width: T_W,
height: T_H,
},
Basis: {
Y: BASIS_Y,
X: BASIS_X,
},
Gantry: {
width: E_W,
height: E_H,
left: E_L,
top: E_T,
},
Nozzle: {
width: N_W,
height: N_H,
left: N_L,
top: N_T,
},
GantryMaxLeft: E_M,
};
}
export const printerConfigAnycubic: AnimatedPrinterConfig = {
top: {
width: 340,
height: 20,
},
bottom: {
width: 340,
height: 52.3,
},
left: {
width: 30,
height: 400,
},
right: {
width: 30,
height: 380,
},
buildplate: {
maxWidth: 250,
maxHeight: 260,
verticalOffset: 55,
},
xAxis: {
stepper: true,
width: 400,
offsetLeft: -30,
height: 30,
extruder: {
width: 60,
height: 100,
},
},
};

View File

@@ -1,961 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { animate, Options as motionOptions } from "@lit-labs/motion";
import { localize } from "../../../../localize/localize";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { platform } from "../../../const";
import { HASSDomEvent } from "../../../fire_event";
import {
getPrinterEntityId,
getPrinterSensorStateObj,
isFDMPrinter,
speedModesFromStateObj,
} from "../../../helpers";
import {
AnycubicPrintOptionConfirmationType,
AnycubicSpeedModeEntity,
AnycubicTargetTempEntity,
DomClickEvent,
DropdownEvent,
EvtTargConfirmationMode,
HassDevice,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
ModalEventBase,
SelectDropdownProps,
TextfieldChangeDetail,
} from "../../../types";
import { commonModalStyle } from "../../ui/modal-styles";
import "../../ui/select-dropdown.ts";
const animOptionsCard: motionOptions = {
keyframeOptions: {
duration: 250,
direction: "alternate",
easing: "ease-in-out",
},
properties: ["height", "opacity", "scale"],
};
@customElementIfUndef("anycubic-printercard-printsettings_modal")
export class AnycubicPrintercardPrintsettingsModal extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@state()
private availableSpeedModes: SelectDropdownProps = {};
@state()
private isFDM: boolean = false;
@state()
private currentSpeedModeKey: number = 0;
@state()
private currentSpeedModeDescr: string | undefined = undefined;
@state()
private _userEditSpeedMode: boolean = false;
@state()
private currentFanSpeed: number = 0;
@state()
private _userEditFanSpeed: boolean = false;
@state()
private currentAuxFanSpeed: number = 0;
@state()
private _userEditAuxFanSpeed: boolean = false;
@state()
private currentBoxFanSpeed: number = 0;
@state()
private _userEditBoxFanSpeed: boolean = false;
@state()
private currentTargetTempNozzle: number = 0;
@state()
private minTargetTempNozzle: number = 0;
@state()
private maxTargetTempNozzle: number = 0;
@state()
private _userEditTargetTempNozzle: boolean = false;
@state()
private currentTargetTempHotbed: number = 0;
@state()
private minTargetTempHotbed: number = 0;
@state()
private maxTargetTempHotbed: number = 0;
@state()
private _userEditTargetTempHotbed: boolean = false;
@state()
private _confirmationType: AnycubicPrintOptionConfirmationType | undefined;
@state()
private _isOpen: boolean = false;
@state()
private _confirmMessage: string;
@state()
private _labelNozzleTemperature: string;
@state()
private _labelHotbedTemperature: string;
@state()
private _labelFanSpeed: string;
@state()
private _labelAuxFanSpeed: string;
@state()
private _labelBoxFanSpeed: string;
@state()
private _buttonYes: string;
@state()
private _buttonNo: string;
@state()
private _buttonPrintPause: string;
@state()
private _buttonPrintResume: string;
@state()
private _buttonPrintCancel: string;
@state()
private _buttonSaveSpeedMode: string;
@state()
private _buttonSaveTargetNozzle: string;
@state()
private _buttonSaveTargetHotbed: string;
@state()
private _buttonSaveFanSpeed: string;
@state()
private _buttonSaveAuxFanSpeed: string;
@state()
private _buttonSaveBoxFanSpeed: string;
@state()
private _changingSettings: boolean = false;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this.addEventListener("ac-select-dropdown", this._handleDropdownEvent);
this.addEventListener("click", (e) => {
this._closeModal(e);
});
}
public connectedCallback(): void {
super.connectedCallback();
this.parentElement?.addEventListener(
"ac-printset-modal",
this._handleModalEvent,
);
}
public disconnectedCallback(): void {
this.parentElement?.removeEventListener(
"ac-printset-modal",
this._handleModalEvent,
);
super.disconnectedCallback();
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._labelNozzleTemperature = localize(
"card.print_settings.label_nozzle_temp",
this.language,
);
this._labelHotbedTemperature = localize(
"card.print_settings.label_hotbed_temp",
this.language,
);
this._labelFanSpeed = localize(
"card.print_settings.label_fan_speed",
this.language,
);
this._labelAuxFanSpeed = localize(
"card.print_settings.label_aux_fan_speed",
this.language,
);
this._labelBoxFanSpeed = localize(
"card.print_settings.label_box_fan_speed",
this.language,
);
this._buttonYes = localize("common.actions.yes", this.language);
this._buttonNo = localize("common.actions.no", this.language);
this._buttonPrintPause = localize(
"card.print_settings.print_pause",
this.language,
);
this._buttonPrintResume = localize(
"card.print_settings.print_resume",
this.language,
);
this._buttonPrintCancel = localize(
"card.print_settings.print_cancel",
this.language,
);
this._buttonSaveSpeedMode = localize(
"card.print_settings.save_speed_mode",
this.language,
);
this._buttonSaveTargetNozzle = localize(
"card.print_settings.save_target_nozzle",
this.language,
);
this._buttonSaveTargetHotbed = localize(
"card.print_settings.save_target_hotbed",
this.language,
);
this._buttonSaveFanSpeed = localize(
"card.print_settings.save_fan_speed",
this.language,
);
this._buttonSaveAuxFanSpeed = localize(
"card.print_settings.save_aux_fan_speed",
this.language,
);
this._buttonSaveBoxFanSpeed = localize(
"card.print_settings.save_box_fan_speed",
this.language,
);
}
if (
changedProperties.has("hass") ||
changedProperties.has("printerEntities") ||
changedProperties.has("printerEntityIdPart")
) {
this.isFDM = isFDMPrinter(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
);
if (!this._userEditFanSpeed) {
this.currentFanSpeed = Number(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"fan_speed",
0,
).state,
);
}
if (!this._userEditTargetTempNozzle) {
const currentTargetTempNozzleState: AnycubicTargetTempEntity =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_nozzle_temperature",
0,
{ limit_min: 0, limit_max: 0 },
) as AnycubicTargetTempEntity;
this.currentTargetTempNozzle = Number(
currentTargetTempNozzleState.state,
);
this.minTargetTempNozzle =
currentTargetTempNozzleState.attributes.limit_min;
this.maxTargetTempNozzle =
currentTargetTempNozzleState.attributes.limit_max;
}
if (!this._userEditTargetTempHotbed) {
const currentTargetTempHotbedState: AnycubicTargetTempEntity =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_hotbed_temperature",
0,
{ limit_min: 0, limit_max: 0 },
) as AnycubicTargetTempEntity;
this.currentTargetTempHotbed = Number(
currentTargetTempHotbedState.state,
);
this.minTargetTempHotbed =
currentTargetTempHotbedState.attributes.limit_min;
this.maxTargetTempHotbed =
currentTargetTempHotbedState.attributes.limit_max;
}
if (!this._userEditSpeedMode) {
const speedModeState: AnycubicSpeedModeEntity =
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_speed_mode",
"",
{ available_modes: [], job_speed_mode_code: -1 },
) as AnycubicSpeedModeEntity;
this.availableSpeedModes = speedModesFromStateObj(
speedModeState,
) as SelectDropdownProps;
this.currentSpeedModeKey =
speedModeState.attributes.print_speed_mode_code;
this.currentSpeedModeDescr =
this.currentSpeedModeKey >= 0 &&
this.currentSpeedModeKey in this.availableSpeedModes
? this.availableSpeedModes[this.currentSpeedModeKey]
: undefined;
}
}
}
protected update(changedProperties: PropertyValues<this>): void {
super.update(changedProperties);
if (this._isOpen) {
this.style.display = "block";
} else {
this.style.display = "none";
}
}
render(): LitTemplateResult {
const stylesMain = {
height: "auto",
opacity: 1.0,
scale: 1.0,
};
return html`
<div
class="ac-modal-container"
style=${styleMap(stylesMain)}
${animate({ ...animOptionsCard })}
>
<span class="ac-modal-close" @click=${this._closeModal}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this._renderCard()}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return this._confirmationType
? this._renderConfirm()
: this._renderSettings();
}
_renderConfirm(): LitTemplateResult {
return html`
<div>
<div class="ac-settings-header">Confirm Action</div>
<div>
<div class="ac-confirm-description">${this._confirmMessage}</div>
<div class="ac-confirm-buttons">
<ha-control-button
@click=${this._handleConfirmApprove}
.disabled=${this._changingSettings}
>
${this._buttonYes}
</ha-control-button>
<ha-control-button @click=${this._handleConfirmCancel}>
${this._buttonNo}
</ha-control-button>
</div>
</div>
</div>
`;
}
_renderSettings(): LitTemplateResult {
return html`
<div>
<div class="ac-settings-header">Print Settings</div>
<div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.PAUSE}
@click=${this._setConfirmationMode}
>
${this._buttonPrintPause}
</ha-control-button>
</div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.RESUME}
@click=${this._setConfirmationMode}
>
${this._buttonPrintResume}
</ha-control-button>
</div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.CANCEL}
@click=${this._setConfirmationMode}
>
${this._buttonPrintCancel}
</ha-control-button>
</div>
${this.isFDM
? html`
<div class="ac-settings-row">
<anycubic-ui-select-dropdown
.availableOptions=${this.availableSpeedModes}
.placeholder=${this.currentSpeedModeDescr}
.initialItem=${this.currentSpeedModeDescr}
></anycubic-ui-select-dropdown>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveSpeedModeButton}
>
${this._buttonSaveSpeedMode}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentTargetTempNozzle}
.placeholder=${this.currentTargetTempNozzle}
.label=${this._labelNozzleTemperature}
.type=${"number"}
.min=${this.minTargetTempNozzle}
.max=${this.maxTargetTempNozzle}
@input=${this._handleTargetTempNozzleChange}
@keydown=${this._handleTargetTempNozzleKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveTargetTempNozzleButton}
>
${this._buttonSaveTargetNozzle}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentTargetTempHotbed}
.placeholder=${this.currentTargetTempHotbed}
.label=${this._labelHotbedTemperature}
.type=${"number"}
.min=${this.minTargetTempHotbed}
.max=${this.maxTargetTempHotbed}
@input=${this._handleTargetTempHotbedChange}
@keydown=${this._handleTargetTempHotbedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveTargetTempHotbedButton}
>
${this._buttonSaveTargetHotbed}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentFanSpeed}
.placeholder=${this.currentFanSpeed}
.label=${this._labelFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleFanSpeedChange}
@keydown=${this._handleFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveFanSpeedButton}
>
${this._buttonSaveFanSpeed}
</ha-control-button>
</div>
<div class="ac-settings-row ac-disabled-feature">
<ha-textfield
.value=${this.currentAuxFanSpeed}
.placeholder=${this.currentAuxFanSpeed}
.label=${this._labelAuxFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleAuxFanSpeedChange}
@keydown=${this._handleAuxFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveAuxFanSpeedButton}
>
${this._buttonSaveAuxFanSpeed}
</ha-control-button>
</div>
<div class="ac-settings-row ac-disabled-feature">
<ha-textfield
.value=${this.currentBoxFanSpeed}
.placeholder=${this.currentBoxFanSpeed}
.label=${this._labelBoxFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleBoxFanSpeedChange}
@keydown=${this._handleBoxFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveBoxFanSpeedButton}
>
${this._buttonSaveBoxFanSpeed}
</ha-control-button>
</div>
`
: nothing}
</div>
</div>
`;
}
private _setConfirmationMode = (
ev: DomClickEvent<EvtTargConfirmationMode>,
): void => {
this._confirmationType = ev.currentTarget.confirmation_type;
this._confirmMessage = localize(
"card.print_settings.confirm_message",
this.language,
"action",
localize(
"common.actions." + (this._confirmationType as string),
this.language,
),
);
};
private _pressHassButton(suffix: string): void {
this._changingSettings = true;
this.hass
.callService("button", "press", {
entity_id: getPrinterEntityId(
this.printerEntityIdPart,
"button",
suffix,
),
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
}
private _handleConfirmApprove = (): void => {
switch (this._confirmationType) {
case AnycubicPrintOptionConfirmationType.PAUSE:
this._pressHassButton("pause_print");
break;
case AnycubicPrintOptionConfirmationType.RESUME:
this._pressHassButton("resume_print");
break;
case AnycubicPrintOptionConfirmationType.CANCEL:
this._pressHassButton("cancel_print");
break;
default:
break;
}
this._confirmationType = undefined;
this._closeModal();
};
private _handleConfirmCancel = (): void => {
this._confirmationType = undefined;
};
private _handleFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentFanSpeed = Number(newSpeed);
this._userEditFanSpeed = true;
};
private _handleAuxFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentAuxFanSpeed = Number(newSpeed);
this._userEditAuxFanSpeed = true;
};
private _handleBoxFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentBoxFanSpeed = Number(newSpeed);
this._userEditBoxFanSpeed = true;
};
private _handleFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedFanSpeed();
} else {
this._userEditFanSpeed = true;
}
};
private _handleAuxFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedAuxFanSpeed();
} else {
this._userEditAuxFanSpeed = true;
}
};
private _handleBoxFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedBoxFanSpeed();
} else {
this._userEditBoxFanSpeed = true;
}
};
private _handleTargetTempNozzleChange = (ev: Event): void => {
const newTemp = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentTargetTempNozzle = Number(newTemp);
this._userEditTargetTempNozzle = true;
};
private _handleTargetTempHotbedChange = (ev: Event): void => {
const newTemp = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentTargetTempHotbed = Number(newTemp);
this._userEditTargetTempHotbed = true;
};
private _handleTargetTempNozzleKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedTargetTempNozzle();
} else {
this._userEditTargetTempNozzle = true;
}
};
private _handleTargetTempHotbedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedTargetTempHotbed();
} else {
this._userEditTargetTempHotbed = true;
}
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventBase>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this._resetUserEdits();
}
};
private _handleDropdownEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<DropdownEvent<number, string>>;
e.stopPropagation();
this._userEditSpeedMode = true;
if (typeof e.detail.key !== "undefined") {
this.currentSpeedModeKey = e.detail.key;
this.currentSpeedModeDescr =
this.currentSpeedModeKey >= 0 &&
this.currentSpeedModeKey in this.availableSpeedModes
? this.availableSpeedModes[this.currentSpeedModeKey]
: undefined;
}
};
private _handleSaveFanSpeedButton = (): void => {
this._submitChangedFanSpeed();
this._resetUserEdits();
};
private _handleSaveAuxFanSpeedButton = (): void => {
this._submitChangedAuxFanSpeed();
this._resetUserEdits();
};
private _handleSaveBoxFanSpeedButton = (): void => {
this._submitChangedBoxFanSpeed();
this._resetUserEdits();
};
private _handleSaveSpeedModeButton = (): void => {
this._submitChangedSpeedMode();
this._resetUserEdits();
};
private _handleSaveTargetTempNozzleButton = (): void => {
this._submitChangedTargetTempNozzle();
this._resetUserEdits();
};
private _handleSaveTargetTempHotbedButton = (): void => {
this._submitChangedTargetTempHotbed();
this._resetUserEdits();
};
private _resetUserEdits(): void {
this._userEditFanSpeed = false;
this._userEditAuxFanSpeed = false;
this._userEditBoxFanSpeed = false;
this._userEditTargetTempNozzle = false;
this._userEditTargetTempHotbed = false;
this._userEditSpeedMode = false;
}
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this._resetUserEdits();
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
private _serviceAvailable(serviceName: string): boolean {
return Boolean(this.hass?.services?.[platform]?.[serviceName]);
}
private _submitChangedSpeedMode(): void {
if (this._userEditSpeedMode && this.selectedPrinterDevice) {
const serv = "change_print_speed_mode";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed_mode: this.currentSpeedModeKey,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedFanSpeed(): void {
if (this._userEditFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedAuxFanSpeed(): void {
if (this._userEditAuxFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_aux_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentAuxFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedBoxFanSpeed(): void {
if (this._userEditBoxFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_box_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentBoxFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedTargetTempNozzle(): void {
if (this._userEditTargetTempNozzle && this.selectedPrinterDevice) {
const serv = "change_print_target_nozzle_temperature";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
temperature: this.currentTargetTempNozzle,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedTargetTempHotbed(): void {
if (this._userEditTargetTempHotbed && this.selectedPrinterDevice) {
const serv = "change_print_target_hotbed_temperature";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
temperature: this.currentTargetTempHotbed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-settings-header {
font-size: 24px;
text-align: center;
font-weight: 600;
margin-bottom: 20px;
}
.ac-settings-row {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
.ac-disabled-feature {
display: none;
}
ha-textfield {
min-width: 150px;
width: 100%;
}
ha-control-button {
min-width: 150px;
margin: 8px 0px 0px 8px;
font-size: 14px;
}
.ac-settings-buttonrow ha-control-button {
min-width: 100%;
margin: 8px 0px 0px 8px;
font-size: 14px;
}
.ac-confirm-description {
font-size: 16px;
text-align: center;
}
.ac-confirm-buttons {
display: flex;
justify-content: center;
}
.ac-confirm-buttons ha-control-button {
margin: 20px 30px 0px 30px;
}
`;
}
}

View File

@@ -1,102 +0,0 @@
import { CSSResult, LitElement, css, html } from "lit";
import { property } from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { LitTemplateResult } from "../../../types";
@customElementIfUndef("anycubic-printercard-progress-line")
export class AnycubicPrintercardProgressLine extends LitElement {
@property({ type: String })
public name: string;
@property({ type: Number })
public value: string;
@property({ type: Number })
public progress: number;
render(): LitTemplateResult {
const progressStyle = {
width: String(this.progress) + "%",
};
return html`
<div class="ac-stat-line">
<p class="ac-stat-heading">${this.name}</p>
<div class="ac-stat-value">
<div class="ac-progress-bar">
<div class="ac-stat-text">${this.value}</div>
<div
class="ac-progress-line"
style=${styleMap(progressStyle)}
></div>
</div>
</div>
</div>
`;
}
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-stat-line {
box-sizing: border-box;
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 2px 0;
}
.ac-stat-value {
margin: 0;
display: inline-block;
max-width: calc(100% - 120px);
width: 100%;
position: relative;
}
.ac-stat-text {
margin: 0;
font-size: 16px;
display: block;
position: relative;
top: 3px;
left: 0px;
z-index: 1;
text-align: center;
}
.ac-stat-heading {
margin: 0;
font-size: 16px;
display: block;
font-weight: bold;
}
.ac-progress-bar {
display: block;
width: 100%;
height: 30px;
background-color: #8b8b8b6e;
position: relative;
}
.ac-progress-line {
position: absolute;
top: 0px;
left: 0px;
display: block;
height: 100%;
background-color: #ee8f36e6;
border-right: 2px solid #ffd151e6;
box-shadow: 4px 0px 6px 0px rgb(255 245 126 / 25%);
}
`;
}
}

View File

@@ -1,60 +0,0 @@
import { CSSResult, LitElement, css, html } from "lit";
import { property } from "lit/decorators.js";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { LitTemplateResult } from "../../../types";
@customElementIfUndef("anycubic-printercard-stat-line")
export class AnycubicPrintercardStatLine extends LitElement {
@property({ type: String })
public name: string;
@property({ type: String })
public value: string;
@property({ type: String })
public unit?: string = "";
render(): LitTemplateResult {
return html`
<div class="ac-stat-line">
<p class="ac-stat-text ac-stat-heading">${this.name}</p>
<p class="ac-stat-text">${this.value}${this.unit}</p>
</div>
`;
}
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-stat-line {
box-sizing: border-box;
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 2px 0;
}
.ac-stat-text {
margin: 0;
font-size: 16px;
display: inline-block;
max-width: calc(100% - 120px);
text-align: right;
word-wrap: break-word;
}
.ac-stat-heading {
font-weight: bold;
max-width: unset;
overflow: unset;
}
`;
}
}

View File

@@ -1,641 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { localize } from "../../../../localize/localize";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import {
getPrinterBinarySensorState,
getPrinterSensorStateObj,
speedModesFromStateObj,
toTitleCase,
} from "../../../helpers";
import {
AnycubicSpeedModeEntity,
HassEntity,
HassEntityInfos,
HomeAssistant,
LitTemplateResult,
PrinterCardStatType,
TemperatureUnit,
TranslationDict,
} from "../../../types";
import "./progress_line.ts";
import "./stat_line.ts";
import "./temperature_stat.ts";
import "./time_stat.ts";
@customElementIfUndef("anycubic-printercard-stats-component")
export class AnycubicPrintercardStatsComponent extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ attribute: "monitored-stats" })
public monitoredStats: PrinterCardStatType[];
@property({ attribute: "show-percent", type: Boolean })
public showPercent?: boolean;
@property({ type: Boolean })
public round?: boolean = true;
@property({ type: Boolean })
public use_24hr?: boolean;
@property({ attribute: "temperature-unit", type: String })
public temperatureUnit: TemperatureUnit = TemperatureUnit.C;
@property({ attribute: "printer-entities" })
public printerEntities: HassEntityInfos;
@property({ attribute: "printer-entity-id-part" })
public printerEntityIdPart: string | undefined;
@property({ attribute: "progress-percent" })
public progressPercent: number = 0;
@state()
private _statTranslations: TranslationDict;
@state()
private _entETA: HassEntity;
@state()
private _entElapsed: HassEntity;
@state()
private _entRemaining: HassEntity;
@state()
private _entBedCurrent: HassEntity;
@state()
private _entHotendCurrent: HassEntity;
@state()
private _entBedTarget: HassEntity;
@state()
private _entHotendTarget: HassEntity;
@state()
private _valStatus: string;
@state()
private _valOnline: string;
@state()
private _valAvailability: string;
@state()
private _valJobName: string;
@state()
private _valCurrentLayer: string;
@state()
private _valSpeedMode: string;
@state()
private _valFanSpeed: string;
@state()
private _valDryStatus: string;
@state()
private _valDryRemain: string;
@state()
private _valDryProgress: number = 0;
@state()
private _valOnTime: string;
@state()
private _valOffTime: string;
@state()
private _valBottomTime: string;
@state()
private _valModelHeight: string;
@state()
private _valBottomLayers: string;
@state()
private _valZUpHeight: string;
@state()
private _valZUpSpeed: string;
@state()
private _valZDownSpeed: string;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("hass") ||
changedProperties.has("printerEntities") ||
changedProperties.has("printerEntityIdPart")
) {
this._entETA = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_time_remaining",
);
this._entElapsed = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_time_elapsed",
);
this._entRemaining = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_time_remaining",
);
this._entBedCurrent = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"hotbed_temperature",
);
this._entHotendCurrent = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"nozzle_temperature",
);
this._entBedTarget = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_hotbed_temperature",
);
this._entHotendTarget = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_nozzle_temperature",
);
this._valStatus = toTitleCase(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_state",
).state,
);
this._valOnline = getPrinterBinarySensorState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"printer_online",
"Online",
"Offline",
"unknown",
) as string;
this._valAvailability = toTitleCase(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"current_status",
).state,
);
this._valJobName = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_name",
).state;
this._valCurrentLayer = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_current_layer",
).state;
const speedModeState: AnycubicSpeedModeEntity = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_speed_mode",
"",
{ available_modes: [], print_speed_mode_code: -1 },
) as AnycubicSpeedModeEntity;
const availableSpeedModes = speedModesFromStateObj(speedModeState);
const currentSpeedModeKey: number =
(speedModeState.attributes.print_speed_mode_code as
| number
| undefined) ?? 0;
this._valSpeedMode =
currentSpeedModeKey >= 0 && currentSpeedModeKey in availableSpeedModes
? availableSpeedModes[currentSpeedModeKey]
: "Unknown";
this._valFanSpeed = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"fan_speed",
0,
).state;
this._valDryStatus = getPrinterBinarySensorState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_active",
"Drying",
"Not Drying",
"unknown",
) as string;
const dryTotal = Number(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_total_duration",
0,
).state,
);
const dryRemain = Number(
getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_remaining_time",
0,
).state,
);
this._valDryRemain = !isNaN(dryRemain) ? `${dryRemain} Mins` : "";
this._valDryProgress =
!isNaN(dryTotal) && dryTotal > 0 ? (dryRemain / dryTotal) * 100 : 0;
this._valOnTime = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_on_time",
0,
).state;
this._valOffTime = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_off_time",
0,
).state;
this._valBottomTime = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_bottom_time",
0,
).state;
this._valModelHeight = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_model_height",
0,
).state;
this._valBottomLayers = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_bottom_layers",
0,
).state;
this._valZUpHeight = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_z_up_height",
0,
).state;
this._valZUpSpeed = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_z_up_speed",
0,
).state;
this._valZDownSpeed = getPrinterSensorStateObj(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_z_down_speed",
0,
).state;
}
if (
changedProperties.has("language") ||
changedProperties.has("monitoredStats")
) {
this._statTranslations = this.monitoredStats.reduce((fConf, statKey) => {
fConf[statKey] = localize(
`card.monitored_stats.${statKey}`,
this.language,
);
return fConf;
}, {});
}
}
render(): LitTemplateResult {
return html`
<div class="ac-stats-box ac-stats-section">
${this.showPercent
? html`
<div class="ac-stats-box ac-stats-part-percent">
<p class="ac-stats-part-percent-text">
${this.round
? Math.round(this.progressPercent)
: this.progressPercent}%
</p>
</div>
`
: null}
<div class="ac-stats-box ac-stats-section">${this._renderStats()}</div>
</div>
`;
}
private _renderStats(): LitTemplateResult {
return repeat(
this.monitoredStats,
(condition) => condition,
(condition, _index): LitTemplateResult => {
switch (condition) {
case PrinterCardStatType.Status:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valStatus}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ETA:
return html`
<anycubic-printercard-stat-time
.timeEntity=${this._entETA}
.timeType=${condition}
.name=${this._statTranslations[condition]}
.direction=${0}
.round=${this.round}
.use_24hr=${this.use_24hr}
></anycubic-printercard-stat-time>
`;
case PrinterCardStatType.Elapsed:
return html`
<anycubic-printercard-stat-time
.timeEntity=${this._entElapsed}
.timeType=${condition}
.name=${this._statTranslations[condition]}
.direction=${1}
.round=${this.round}
.use_24hr=${this.use_24hr}
></anycubic-printercard-stat-time>
`;
case PrinterCardStatType.Remaining:
return html`
<anycubic-printercard-stat-time
.timeEntity=${this._entRemaining}
.timeType=${condition}
.name=${this._statTranslations[condition]}
.direction=${-1}
.round=${this.round}
.use_24hr=${this.use_24hr}
></anycubic-printercard-stat-time>
`;
case PrinterCardStatType.BedCurrent:
return html`
<anycubic-printercard-stat-temperature
.name=${this._statTranslations[condition]}
.temperatureEntity=${this._entBedCurrent}
.round=${this.round}
.temperatureUnit=${this.temperatureUnit}
></anycubic-printercard-stat-temperature>
`;
case PrinterCardStatType.HotendCurrent:
return html`
<anycubic-printercard-stat-temperature
.name=${this._statTranslations[condition]}
.temperatureEntity=${this._entHotendCurrent}
.round=${this.round}
.temperatureUnit=${this.temperatureUnit}
></anycubic-printercard-stat-temperature>
`;
case PrinterCardStatType.BedTarget:
return html`
<anycubic-printercard-stat-temperature
.name=${this._statTranslations[condition]}
.temperatureEntity=${this._entBedTarget}
.round=${this.round}
.temperatureUnit=${this.temperatureUnit}
></anycubic-printercard-stat-temperature>
`;
case PrinterCardStatType.HotendTarget:
return html`
<anycubic-printercard-stat-temperature
.name=${this._statTranslations[condition]}
.temperatureEntity=${this._entHotendTarget}
.round=${this.round}
.temperatureUnit=${this.temperatureUnit}
></anycubic-printercard-stat-temperature>
`;
case PrinterCardStatType.PrinterOnline:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valOnline}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.Availability:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valAvailability}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ProjectName:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valJobName}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.CurrentLayer:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valCurrentLayer}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.SpeedMode:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valSpeedMode}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.FanSpeed:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valFanSpeed}
.unit=${"%"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.DryingStatus:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valDryStatus}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.DryingTime:
return html`
<anycubic-printercard-progress-line
.name=${this._statTranslations[condition]}
.value=${this._valDryRemain}
.progress=${this._valDryProgress}
></anycubic-printercard-progress-line>
`;
case PrinterCardStatType.OnTime:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valOnTime}
.unit=${"s"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.OffTime:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valOffTime}
.unit=${"s"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.BottomTime:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valBottomTime}
.unit=${"s"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ModelHeight:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valModelHeight}
.unit=${"mm"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.BottomLayers:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valBottomLayers}
.unit=${"layers"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ZUpHeight:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valZUpHeight}
.unit=${"mm"}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ZUpSpeed:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valZUpSpeed}
></anycubic-printercard-stat-line>
`;
case PrinterCardStatType.ZDownSpeed:
return html`
<anycubic-printercard-stat-line
.name=${this._statTranslations[condition]}
.value=${this._valZDownSpeed}
></anycubic-printercard-stat-line>
`;
default:
return html`
<anycubic-printercard-stat-line
.name=${"Unknown"}
.value=${"<unknown>"}
></anycubic-printercard-stat-line>
`;
}
},
) as LitTemplateResult;
}
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-stats-box {
box-sizing: border-box;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.ac-stats-section {
flex-direction: column;
justify-content: center;
}
.ac-stats-part-percent {
justify-content: center;
margin-bottom: 20px;
}
.ac-stats-part-percent-text {
margin: 0px;
font-size: 42px;
font-weight: bold;
height: 44px;
line-height: 44px;
}
`;
}
}

View File

@@ -1,44 +0,0 @@
import { CSSResult, LitElement, css, html } from "lit";
import { property } from "lit/decorators.js";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { getEntityTemperature } from "../../../helpers";
import { HassEntity, LitTemplateResult, TemperatureUnit } from "../../../types";
import "./stat_line.ts";
@customElementIfUndef("anycubic-printercard-stat-temperature")
export class AnycubicPrintercardStatTemperature extends LitElement {
@property({ type: String })
public name: string;
@property({ attribute: "temperature-entity" })
public temperatureEntity: HassEntity;
@property({ type: Boolean })
public round?: boolean;
@property({ attribute: "temperature-unit", type: String })
public temperatureUnit: TemperatureUnit;
render(): LitTemplateResult {
return html`<anycubic-printercard-stat-line
.name=${this.name}
.value=${getEntityTemperature(
this.temperatureEntity,
this.temperatureUnit,
this.round,
)}
></anycubic-printercard-stat-line>`;
}
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
`;
}
}

View File

@@ -1,108 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { customElementIfUndef } from "../../../internal/register-custom-element";
import { calculateTimeStat, getEntityTotalSeconds } from "../../../helpers";
import {
CalculatedTimeType,
HassEntity,
LitTemplateResult,
} from "../../../types";
import "./stat_line.ts";
@customElementIfUndef("anycubic-printercard-stat-time")
export class AnycubicPrintercardStatTime extends LitElement {
@property({ attribute: "time-entity" })
public timeEntity: HassEntity;
@property({ attribute: "time-type" })
public timeType: CalculatedTimeType;
@property({ type: String })
public name: string;
@property({ type: Number })
public direction: number;
@property({ type: Boolean })
public round?: boolean;
@property({ type: Boolean })
public use_24hr?: boolean;
@property({ attribute: "is-seconds", type: Boolean })
public isSeconds?: boolean;
@state()
private currentTime: number | string | undefined = 0;
@state()
private lastIntervalId: number = -1;
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!changedProperties.has("timeEntity")) {
return;
}
if (this.lastIntervalId !== -1) {
clearInterval(this.lastIntervalId);
}
this.currentTime = getEntityTotalSeconds(this.timeEntity);
this.lastIntervalId = setInterval(() => {
this._incTime();
}, 1000);
}
public connectedCallback(): void {
super.connectedCallback();
if (this.lastIntervalId === -1) {
this.lastIntervalId = setInterval(() => {
this._incTime();
}, 1000);
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this.lastIntervalId !== -1) {
clearInterval(this.lastIntervalId);
this.lastIntervalId = -1;
}
}
render(): LitTemplateResult {
return html`<anycubic-printercard-stat-line
.name=${this.name}
.value=${calculateTimeStat(
this.currentTime,
this.timeType,
this.round,
this.use_24hr,
)}
></anycubic-printercard-stat-line>`;
}
private _incTime(): void {
if (
this.currentTime === 0 ||
(this.currentTime && !isNaN(this.currentTime as number))
) {
this.currentTime = Number(this.currentTime) + this.direction;
}
}
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
`;
}
}

View File

@@ -1,56 +0,0 @@
import { CSSResult, css } from "lit";
export const commonModalStyle: CSSResult = css`
:host {
display: none;
position: fixed;
z-index: 10;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(3px);
}
.ac-modal-container {
border-radius: 16px;
background-color: var(--primary-background-color);
margin: auto;
padding: 50px;
width: 80%;
min-height: 150px;
max-width: 600px;
margin-top: 50px;
box-shadow: 0px 0px 15px 5px rgba(0, 0, 0, 0.3);
}
.ac-modal-card {
padding: 20px;
}
.ac-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.ac-modal-close:hover,
.ac-modal-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.ac-modal-label {
}
@media (max-width: 599px) {
.ac-modal-container {
width: 95%;
padding: 6px;
}
}
`;

View File

@@ -1,284 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { mdiCheck, mdiChevronDown, mdiChevronUp } from "@mdi/js";
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { customElementIfUndef } from "../../internal/register-custom-element";
import {
DomClickEvent,
EvtTargDirection,
LitTemplateResult,
} from "../../types";
@customElementIfUndef("anycubic-ui-multi-select-reorder-item")
export class AnycubicUIMultiSelectReorderItem extends LitElement {
@property()
public item: any;
@property({ attribute: "selected-items" })
public selectedItems: any[];
@property({ attribute: "unused-items" })
public unusedItems: any[];
@property()
public reorder: (item: any, mod: number) => void;
@property()
public toggle: (item: any) => void;
@state()
private _isActive: boolean;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("selectedItems") ||
changedProperties.has("item")
) {
this._isActive = this.selectedItems.includes(this.item);
}
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (
changedProperties.has("_isActive") ||
changedProperties.has("selectedItems") ||
changedProperties.has("unusedItems") ||
changedProperties.has("item")
) {
this.style.top =
String(
this._isActive
? 56 * this.selectedItems.indexOf(this.item)
: 56 *
(this.selectedItems.length +
this.unusedItems.indexOf(this.item)),
) + "px";
}
}
render(): LitTemplateResult {
const classesItemText = {
"ac-ui-deselected": !this._isActive,
};
return html`
<button class="ac-ui-msr-select" @click=${this._toggle_item}>
${this._isActive
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: nothing}
</button>
<p class="ac-ui-msr-itemtext ${classMap(classesItemText)}">
${this.item}
</p>
<div>
<button
class="ac-ui-msr-position"
.direction=${1}
@click=${this._reorder_item}
>
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
</button>
<button
class="ac-ui-msr-position"
.direction=${-1}
@click=${this._reorder_item}
>
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
</button>
</div>
`;
}
private _toggle_item = (): void => {
this.toggle(this.item);
};
private _reorder_item = (ev: DomClickEvent<EvtTargDirection>): void => {
if (this._isActive) {
this.reorder(this.item, ev.currentTarget.direction);
}
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
display: flex;
justify-content: space-between;
align-items: center;
}
.ac-ui-msr-itemtext {
flex-grow: 1;
font-size: 16px;
font-weight: bold;
line-height: 24px;
}
.ac-ui-msr-select {
box-sizing: border-box;
width: 24px;
height: 24px;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.1);
outline: none;
border: none;
margin-right: 16px;
padding: 0px;
display: flex;
justify-content: center;
align-items: center;
color: var(--primary-text-color);
cursor: pointer;
}
.ac-ui-msr-position {
box-sizing: border-box;
width: 24px;
height: 24px;
border-radius: 8px;
background-color: transparent;
outline: none;
border: none;
margin-left: 16px;
color: var(--primary-text-color);
cursor: pointer;
}
`;
}
}
@customElementIfUndef("anycubic-ui-multi-select-reorder")
export class AnycubicUIMultiSelectReorder extends LitElement {
@property({ attribute: "available-options" })
public availableOptions: object;
@property({ attribute: "initial-items" })
public initialItems: (string | number)[];
@property({ attribute: "on-change" })
public onChange: (sel: (string | number)[]) => void;
@state()
private _allOptions: (string | number)[];
@state()
private _selectedItems: (string | number)[];
@state()
private _unusedItems: (string | number)[];
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this._allOptions = Object.values(this.availableOptions) as (
| string
| number
)[];
this._setSelectedItems(
[...this.initialItems].filter((item: string | number) =>
this._allOptions.includes(item),
),
);
this._unusedItems = this._allOptions.filter(
(item) => !this.initialItems.includes(item),
);
this.requestUpdate();
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
}
render(): LitTemplateResult {
const stylesCont = {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
height: this._allOptions
? String(this._allOptions.length * 56) + "px"
: "0px",
};
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return this._allOptions
? html`
<div style=${styleMap(stylesCont)}>
${map(this._allOptions, (item, _index) => {
return html`
<anycubic-ui-multi-select-reorder-item
.item=${item}
.selectedItems=${this._selectedItems}
.unusedItems=${this._unusedItems}
.reorder=${this._reorder}
.toggle=${this._toggle}
></anycubic-ui-multi-select-reorder-item>
`;
})}
</div>
`
: nothing;
}
private _setSelectedItems(selectedItems: (string | number)[]): void {
this._selectedItems = selectedItems;
this.onChange(this._selectedItems);
}
private _reorder = (item: string | number, mod: number): void => {
const ind = this._selectedItems.indexOf(item);
const newPos = ind + mod;
if (newPos < 0 || newPos > this._selectedItems.length - 1) {
return;
}
const clone = this._selectedItems.slice(0);
const tmp = clone[newPos];
clone[newPos] = item;
clone[ind] = tmp;
this._setSelectedItems(clone);
};
private _toggle = (item: string | number): void => {
if (this._selectedItems.includes(item)) {
const i = this._selectedItems.indexOf(item);
this._setSelectedItems([
...this._selectedItems.slice(0, i),
...this._selectedItems.slice(i + 1),
]);
this._unusedItems = [item, ...this._unusedItems];
} else {
const i = this._unusedItems.indexOf(item);
this._unusedItems = [
...this._unusedItems.slice(0, i),
...this._unusedItems.slice(i + 1),
];
this._setSelectedItems([...this._selectedItems, item]);
}
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}
`;
}
}

View File

@@ -1,227 +0,0 @@
import { mdiChevronDown } from "@mdi/js";
import { CSSResult, LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import { styleMap } from "lit/directives/style-map.js";
import { customElementIfUndef } from "../../internal/register-custom-element";
import { fireEvent } from "../../fire_event";
import { DomClickEvent, EvtTargItemKey, LitTemplateResult } from "../../types";
@customElementIfUndef("anycubic-ui-select-dropdown-item")
export class AnycubicUISelectDropdownItem extends LitElement {
@property()
public item: string;
@state()
private _isActive: boolean = false;
render(): LitTemplateResult {
const stylesOption = {
filter: this._isActive ? "brightness(80%)" : "brightness(100%)",
};
return html`
<button
class="ac-ui-seld-select"
style=${styleMap(stylesOption)}
@mouseenter=${this._setActive}
@mousedown=${this._setActive}
@mouseup=${this._setInactive}
@mouseleave=${this._setInactive}
>
${this.item}
</button>
`;
}
private _setActive = (): void => {
this._isActive = true;
};
private _setInactive = (): void => {
this._isActive = false;
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
}
.ac-ui-seld-select {
width: 100%;
border: none;
outline: none;
background: var(
--ha-card-background,
var(--card-background-color, white)
);
padding: 0 16px;
box-sizing: border-box;
font-size: 16px;
font-weight: bold;
line-height: 48px;
text-align: left;
cursor: pointer;
color: var(--primary-text-color);
}
`;
}
}
@customElementIfUndef("anycubic-ui-select-dropdown")
export class AnycubicUISelectDropdown extends LitElement {
@property({ attribute: "available-options" })
public availableOptions?: object;
@property()
public placeholder: string;
@property({ attribute: "initial-item" })
public initialItem: string | undefined;
@state()
private _selectedItem: string | undefined;
@state()
private _active: boolean = false;
@state()
private _hidden: boolean = false;
// eslint-disable-next-line @typescript-eslint/require-await
async firstUpdated(): Promise<void> {
this._selectedItem = this.initialItem;
this._hidden = true;
this._active = false;
this.requestUpdate();
}
render(): LitTemplateResult {
const stylesButton = {
backgroundColor: this._active ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.15)",
};
const stylesOptions = {
opacity: this._hidden ? 0.0 : 1.0,
transform: this._hidden ? "scaleY(0.0)" : "scaleY(1.0)",
};
return this.availableOptions
? html`
<button
class="ac-ui-select-button"
style=${styleMap(stylesButton)}
@click=${this._showOptions}
@mouseenter=${this._setActive}
@mouseleave=${this._setInactive}
>
${this._selectedItem ? this._selectedItem : this.placeholder}
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
</button>
<div class="ac-ui-select-options" style=${styleMap(stylesOptions)}>
${this._renderOptions()}
</div>
`
: nothing;
}
private _renderOptions(): Generator<unknown, void, LitTemplateResult> {
return map(
Object.keys(this.availableOptions as object),
(key: string | number, _index: number): LitTemplateResult => {
return html`
<anycubic-ui-select-dropdown-item
.item=${(this.availableOptions as object)[key]}
.item_key=${key}
@click=${this._selectItem}
></anycubic-ui-select-dropdown-item>
`;
},
);
}
private _showOptions = (): void => {
this._hidden = false;
};
private _hideOptions = (): void => {
this._hidden = true;
};
private _setActive = (): void => {
this._active = true;
};
private _setInactive = (): void => {
this._active = false;
};
private _selectItem = (ev: DomClickEvent<EvtTargItemKey>): void => {
if (!this.availableOptions) {
return;
}
const key = ev.currentTarget.item_key;
this._selectedItem = this.availableOptions[key] as string | undefined;
fireEvent(this, "ac-select-dropdown", {
key: key,
value: this.availableOptions[key] as string | undefined,
});
this._hidden = true;
};
static get styles(): CSSResult {
return css`
:host {
box-sizing: border-box;
width: 100%;
position: relative;
background: var(
--ha-card-background,
var(--card-background-color, white)
);
border-radius: 8px;
}
.ac-ui-select-button {
width: 100%;
border: none;
outline: none;
padding: 0 16px;
box-sizing: border-box;
font-size: 16px;
font-weight: bold;
line-height: 48px;
border-radius: 8px;
text-align: left;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.05);
align-items: center;
color: var(--primary-text-color);
}
.ac-ui-select-options {
width: 100%;
position: absolute;
top: 0px;
left: 0px;
box-sizing: border-box;
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
box-shadow:
0px 10px 20px rgba(0, 0, 0, 0.19),
0px 6px 6px rgba(0, 0, 0, 0.23);
z-index: 11;
opacity: 0;
transform: scaleY(0);
transform-origin: top center;
}
`;
}
}

View File

@@ -1,7 +0,0 @@
export const platform = "kobrax_lan";
export const DEBUG = false;
export const LIGHT_ENTITY_DOMAINS = ["light"];
export const SWITCH_ENTITY_DOMAINS = ["switch"];
export const CAMERA_ENTITY_DOMAINS = ["camera"];

View File

@@ -1,65 +0,0 @@
// Polymer legacy event helpers used courtesy of the Polymer project.
//
// Copyright (c) 2017 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/* global HASSDomEvents */
declare global {
// tslint:disable-next-line
interface HASSDomEvents {}
}
export type ValidHassDomEvent = keyof HASSDomEvents;
export interface HASSDomEvent<T> extends Event {
detail: T;
}
export const fireEvent = (
node: HTMLElement | Window,
type: string,
evt_detail?: object | null,
evt_options?: {
bubbles?: boolean;
cancelable?: boolean;
composed?: boolean;
},
): Event => {
const options = evt_options || {};
const detail =
evt_detail === null || evt_detail === undefined ? {} : evt_detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed,
});
(event as HASSDomEvent<typeof detail>).detail = detail;
node.dispatchEvent(event);
return event;
};

View File

@@ -1,23 +0,0 @@
enum HapticStrength {
Light = "light",
Medium = "medium",
Heavy = "heavy",
}
interface HapticEvent extends Event {
detail: HapticStrength;
}
const fireHaptic = (
hapticStrength: HapticStrength = HapticStrength.Medium,
): void => {
const event: HapticEvent = new Event("haptic") as HapticEvent;
event.detail = hapticStrength;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (window) {
window.dispatchEvent(event);
}
};
export { fireHaptic, HapticStrength };

View File

@@ -1,840 +0,0 @@
import { utc as dfnsUtc } from "@date-fns/utc";
import {
Duration as dfnsDuration,
format as dfnsFormat,
intervalToDuration as dfnsIntervalToDuration,
} from "date-fns";
import { fireEvent } from "./fire_event";
import {
AnycubicCardConfig,
AnycubicLitNode,
AnycubicMaterialType,
AnycubicSpeedMode,
AnycubicSpeedModeEntity,
AnycubicSpeedModes,
CalculatedTimeType,
HassDevice,
HassDeviceList,
HassEmptyEntity,
HassEntity,
HassEntityInfo,
HassEntityInfos,
HassRoute,
HomeAssistant,
PrinterCardStatType,
TemperatureUnit,
} from "./types";
const stylePxKeys = ["width", "height", "left", "top"];
export function updateElementStyleWithObject(
el: HTMLElement | undefined,
updateObj: any, // eslint-disable-line
): void {
Object.keys(updateObj as object).forEach((key) => {
// eslint-disable-next-line
if (stylePxKeys.includes(key) && !isNaN(updateObj[key])) {
// eslint-disable-next-line
updateObj[key] = (updateObj[key].toString()) + "px";
}
});
if (el) {
Object.assign(el.style, updateObj);
}
}
export function createEmptyEntity(entityParams: HassEmptyEntity): HassEntity {
return {
state: entityParams.state,
attributes: entityParams.attributes,
entity_id: "invalid_domain.invalid_entity",
last_changed: "",
last_updated: "",
context: {
id: "",
parent_id: null,
user_id: null,
},
};
}
export function numberFromString(str: string): number {
const matches = str.match(/\d+/);
return Number(matches ? matches[0] : -1);
}
export function toTitleCase(str: string): string {
return str
.toLowerCase()
.split(" ")
.map((word: string) => {
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
}
export function buildImageUrlFromEntity(entityState: HassEntity): string {
const token: string = entityState.attributes.access_token as string;
return `${window.location.origin}/api/image_proxy/${entityState.entity_id}?token=${token}`;
}
export function buildCameraUrlFromEntity(entityState: HassEntity): string {
const token: string = entityState.attributes.access_token as string;
return `${window.location.origin}/api/camera_proxy_stream/${entityState.entity_id}?token=${token}`;
}
export function prettyFilename(str: string): string {
const splitI = str.indexOf("-0.");
const splitName =
splitI > 0 ? [str.slice(0, splitI), str.slice(splitI + 1)] : [str];
const chunksFirst = splitName[0].match(/.{1,10}/g);
const joinFirst = chunksFirst ? chunksFirst.join("\n") : splitName[0];
return splitName.length > 1
? joinFirst + "-" + splitName.slice(1)[0]
: joinFirst;
}
export function getEntityState(
hass: HomeAssistant,
entityInfo: HassEntityInfo | undefined,
): HassEntity | undefined {
return entityInfo ? hass.states[entityInfo.entity_id] : undefined;
}
export function getEntityStateFloat(
hass: HomeAssistant,
entityInfo: HassEntityInfo | undefined,
): number {
const entityState = getEntityState(hass, entityInfo);
const stateFloat = entityState ? parseFloat(entityState.state) : 0;
return !isNaN(stateFloat) ? stateFloat : 0;
}
export function getEntityStateString(
hass: HomeAssistant,
entityInfo: HassEntityInfo | undefined,
): string {
const entityState = getEntityState(hass, entityInfo);
return entityState ? String(entityState.state) : "";
}
export function getEntityStateBinary(
hass: HomeAssistant,
entityInfo: HassEntityInfo | undefined,
onValue: string | boolean,
offValue: string | boolean,
): string | boolean {
const entityState = getEntityStateString(hass, entityInfo);
return entityState === "on" ? onValue : offValue;
}
export function getPrinterDevices(hass: HomeAssistant): HassDeviceList {
const printers: HassDeviceList = {};
for (const key in hass.devices) {
const dev = hass.devices[key];
if (dev.manufacturer === "Anycubic") {
printers[dev.id] = dev;
}
}
return printers;
}
export function getPrinterEntities(
hass: HomeAssistant,
deviceID: string | undefined,
): HassEntityInfos {
const entities: HassEntityInfos = {};
if (deviceID) {
for (const key in hass.entities) {
const ent = hass.entities[key];
if (ent.device_id === deviceID) {
entities[ent.entity_id] = ent;
}
}
}
return entities;
}
export function getMatchingEntity(
entities: HassEntityInfos,
match_domain: string,
match_suffix: string,
): HassEntityInfo | undefined {
for (const key in entities) {
const ent = entities[key];
const splitID = key.split(".");
const domain: string = splitID[0];
const entity_id: string = splitID[1];
if (domain === match_domain && entity_id.endsWith(match_suffix)) {
return ent;
}
}
return undefined;
}
export function getPrinterEntityId(
printerEntityIdPart: string | undefined,
domain: string,
suffix: string,
): string {
return domain + "." + String(printerEntityIdPart) + suffix;
}
export function getStrictMatchingEntity(
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
match_domain: string,
match_suffix: string,
): HassEntityInfo | undefined {
if (!printerEntityIdPart) {
return undefined;
}
for (const key in entities) {
const ent = entities[key];
const splitID = key.split(".");
const domain: string = splitID[0];
const entityIdPart: string = splitID[1].split(printerEntityIdPart)[1];
if (domain === match_domain && entityIdPart === match_suffix) {
return ent;
}
}
return undefined;
}
export function getPrinterEntityIdPart(
entities: HassEntityInfos,
): string | undefined {
for (const key in entities) {
const splitID = key.split(".");
const domain: string = splitID[0];
const entity_id: string = splitID[1];
if (domain === "binary_sensor" && entity_id.endsWith("printer_online")) {
return entity_id.split("printer_online")[0];
}
}
return undefined;
}
export function getPrinterSwitchStateObj(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
): HassEntity | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"switch",
suffix,
);
const stateObj = getEntityState(hass, entInfo);
return stateObj;
}
export function getPrinterSwitchState(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
onValue: string | boolean = true,
offValue: string | boolean = false,
): string | boolean | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"switch",
suffix,
);
return entInfo
? getEntityStateBinary(hass, entInfo, onValue, offValue)
: undefined;
}
export function getPrinterButtonStateObj(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
defaultState: string | number = "unavailable",
defaultAttributes: object = {},
): HassEntity {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"button",
suffix,
);
const stateObj = getEntityState(hass, entInfo);
return (
stateObj ||
createEmptyEntity({
state: String(defaultState),
attributes: defaultAttributes,
})
);
}
export function getPrinterDryingButtonStateObj(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
): HassEntity {
return getPrinterButtonStateObj(
hass,
entities,
printerEntityIdPart,
suffix,
"unavailable",
{ duration: 0, temperature: 0 },
);
}
export function isPrinterButtonStateAvailable(stateObj: HassEntity): boolean {
return !["unavailable"].includes(stateObj.state);
}
export function getPrinterImageStateUrl(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
): string | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"image",
suffix,
);
const stateObj = getEntityState(hass, entInfo);
return stateObj ? buildImageUrlFromEntity(stateObj) : undefined;
}
export function getPrinterSensorStateObj(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
defaultState: string | number = "unavailable",
defaultAttributes: object = {},
): HassEntity {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"sensor",
suffix,
);
const stateObj = getEntityState(hass, entInfo);
return (
stateObj ||
createEmptyEntity({
state: String(defaultState),
attributes: defaultAttributes,
})
);
}
export function getPrinterSensorStateString(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
titleCase: boolean = false,
): string | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"sensor",
suffix,
);
if (entInfo) {
const str = getEntityStateString(hass, entInfo);
if (titleCase) {
return toTitleCase(str);
} else {
return str;
}
} else {
return undefined;
}
}
export function getPrinterSensorStateFloat(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
): number | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"sensor",
suffix,
);
return entInfo ? getEntityStateFloat(hass, entInfo) : undefined;
}
export function getPrinterBinarySensorState(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
onValue: string | boolean,
offValue: string | boolean,
undefValue: string | boolean | undefined = undefined,
): string | boolean | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"binary_sensor",
suffix,
);
return entInfo
? getEntityStateBinary(hass, entInfo, onValue, offValue)
: undefValue;
}
export function getPrinterUpdateEntityState(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
suffix: string,
): string | undefined {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"update",
suffix,
);
if (entInfo) {
return getEntityStateBinary(
hass,
entInfo,
"Update Available",
"Up To Date",
) as string;
} else {
return undefined;
}
}
export function getPrinterSupportsMQTT(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
): boolean {
const entInfo = getStrictMatchingEntity(
entities,
printerEntityIdPart,
"binary_sensor",
"mqtt_connection_active",
);
const stateObj = getEntityState(hass, entInfo);
return stateObj ? !!stateObj.attributes.supports_mqtt_login : false;
}
export function isFDMPrinter(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
): boolean {
return (
getPrinterSensorStateObj(
hass,
entities,
printerEntityIdPart,
"current_status",
).attributes.material_type === "Filament"
);
}
export function isLCDPrinter(
hass: HomeAssistant,
entities: HassEntityInfos,
printerEntityIdPart: string | undefined,
): boolean {
return (
getPrinterSensorStateObj(
hass,
entities,
printerEntityIdPart,
"current_status",
).attributes.material_type === "Resin"
);
}
export function getFileListLocalFilesEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "sensor", "file_list_local");
}
export function getFileListLocalRefreshEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "button", "request_file_list_local");
}
export function getFileListUdiskFilesEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "sensor", "file_list_udisk");
}
export function getFileListUdiskRefreshEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "button", "request_file_list_udisk");
}
export function getFileListCloudFilesEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "sensor", "file_list_cloud");
}
export function getFileListCloudRefreshEntity(
entities: HassEntityInfos,
): HassEntityInfo | undefined {
return getMatchingEntity(entities, "button", "request_file_list_cloud");
}
export function getPrinterDevID(route: HassRoute): string | undefined {
const pathParts = route.path.split("/");
return pathParts.length > 1 ? pathParts[1] : undefined;
}
export function getSelectedPrinter(
deviceList: HassDeviceList | undefined,
deviceID: string | undefined,
): HassDevice | undefined {
return deviceList && deviceID ? deviceList[deviceID] : undefined;
}
export function getPrinterMAC(printer: HassDevice | undefined): string | null {
return printer &&
printer.connections.length > 0 &&
printer.connections[0].length > 1
? printer.connections[0][1]
: null;
}
export function getPrinterID(
printer: HassDevice | undefined,
): string | undefined {
return printer ? printer.serial_number : undefined;
}
export function getPage(route: HassRoute): string {
const pathParts = route.path.split("/");
return pathParts.length > 2 ? pathParts[2] : "main";
}
export function isPrintStatePrinting(printStateString: string): boolean {
return [
"printing",
"preheating",
"paused",
"downloading",
"checking",
].includes(printStateString);
}
export function printStateStatusColor(printStateString: string): string {
if (printStateString === "preheating") {
return "#ffc107";
} else if (isPrintStatePrinting(printStateString)) {
return "#4caf50";
} else if (printStateString === "unknown") {
return "#f44336";
} else if (
printStateString === "operational" ||
printStateString === "finished"
) {
return "#00bcd4";
} else {
return "#f44336";
}
}
export const navigateToPrinter = (
node: AnycubicLitNode,
printerID: string,
replace: boolean = false,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const prefix: string = node.route.prefix;
const endpoint = printerID ? `${printerID}/main` : "";
const url = `${prefix}/${endpoint}`;
if (replace) {
history.replaceState(null, "", url);
} else {
history.pushState(null, "", url);
}
fireEvent(window, "location-changed", {
replace,
});
};
export const navigateToPage = (
node: AnycubicLitNode,
path: string,
replace: boolean = false,
): void => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const prefix: string = node.route.prefix;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const printerID = getPrinterDevID(node.route);
const endpoint = printerID ? `${printerID}/${path}` : "";
const url = `${prefix}/${endpoint}`;
if (replace) {
history.replaceState(null, "", url);
} else {
history.pushState(null, "", url);
}
fireEvent(window, "location-changed", {
replace,
});
};
export function milliSecondsToDuration(milliSeconds: number): dfnsDuration {
const epoch = new Date(0);
const secondsAfterEpoch = new Date(milliSeconds);
return dfnsIntervalToDuration({
start: epoch,
end: secondsAfterEpoch,
});
}
export function secondsToDuration(seconds: number): dfnsDuration {
return milliSecondsToDuration(seconds * 1e3);
}
export const formatDuration = (
time: number | string | undefined,
round: boolean,
): string => {
if (time !== 0 && (!time || isNaN(time as number))) {
return "invalid duration";
}
const dur: dfnsDuration = secondsToDuration(
round ? Math.ceil(Number(time) / 60) * 60 : Number(time),
);
const days: string = dur.days && dur.days > 0 ? `${dur.days}d` : "";
const hours: string = dur.hours && dur.hours > 0 ? `${dur.hours}h` : "";
const minutes: string =
dur.minutes && dur.minutes > 0 ? `${dur.minutes}m` : "";
const seconds: string =
dur.seconds && dur.seconds > 0 ? `${dur.seconds}s` : round ? "" : "0s";
return `${days}${hours}${minutes}${seconds}`;
};
export const formatFutureTime = (
futureSeconds: number | string | undefined,
round: boolean,
use_24hr: boolean,
): string => {
if (
futureSeconds !== 0 &&
(!futureSeconds || isNaN(futureSeconds as number))
) {
return "invalid time";
}
const fmtSeconds = round ? "" : ":ss";
const fmtString = use_24hr ? `HH:mm${fmtSeconds}` : `h:mm${fmtSeconds} a`;
const newDate = new Date();
newDate.setSeconds(newDate.getSeconds() + Number(futureSeconds));
return dfnsFormat(newDate, fmtString, { in: dfnsUtc });
};
export const calculateTimeStat = (
time: number | string | undefined,
timeType: CalculatedTimeType,
round: boolean = false,
use_24hr: boolean = false,
): string => {
switch (timeType) {
case CalculatedTimeType.Remaining:
return formatDuration(time, round);
case CalculatedTimeType.ETA:
return formatFutureTime(time, round, use_24hr);
case CalculatedTimeType.Elapsed:
return formatDuration(time, round);
default:
return "<unknown>";
}
};
export function getEntityTotalSeconds(
timeEntity: HassEntity,
isSeconds: boolean = false,
): number {
let result: number;
if (timeEntity.state) {
if (timeEntity.state.includes(", ")) {
const [days_string, time_string] = timeEntity.state.split(", ");
const [hours, minutes, seconds] = time_string.split(":");
const day_match = days_string.match(/\d+/);
const days = day_match ? day_match[0] : 0;
result =
+days * 60 * 60 * 24 + +hours * 60 * 60 + +minutes * 60 + +seconds;
} else if (timeEntity.state.includes(":")) {
const [hours, minutes, seconds] = timeEntity.state.split(":");
result = +hours * 60 * 60 + +minutes * 60 + +seconds;
} else if (isSeconds) {
const seconds = timeEntity.state;
result = +seconds;
} else {
const minutes = timeEntity.state;
result = +minutes * 60;
}
} else {
result = 0;
}
return result;
}
export const temperatureUnitFromEntity = (
entity: HassEntity,
): TemperatureUnit => {
switch (entity.attributes.unit_of_measurement) {
case "°C":
return TemperatureUnit.C;
case "°F":
return TemperatureUnit.F;
default:
return TemperatureUnit.C;
}
};
const temperatureMap = {
[TemperatureUnit.C]: {
[TemperatureUnit.C]: (t: number): number => t,
[TemperatureUnit.F]: (t: number): number => (t * 9.0) / 5.0 + 32.0,
},
[TemperatureUnit.F]: {
[TemperatureUnit.C]: (t: number): number => ((t - 32.0) * 5.0) / 9.0,
[TemperatureUnit.F]: (t: number): number => t,
},
};
export const convertTemperature = (
temperature: number,
from: TemperatureUnit,
to: TemperatureUnit,
): number => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!temperatureMap[from] || !temperatureMap[from][to]) {
return -1;
}
return temperatureMap[from][to](temperature);
};
export const getEntityTemperature = (
temperatureEntity: HassEntity,
temperatureUnit: TemperatureUnit | undefined,
round: boolean = false,
): string => {
const t: number = parseFloat(temperatureEntity.state);
const u: TemperatureUnit = temperatureUnitFromEntity(temperatureEntity);
const tc: number = convertTemperature(t, u, temperatureUnit || u);
return `${round ? Math.round(tc) : tc.toFixed(2)}°${temperatureUnit || u}`;
};
export function getDefaultMonitoredStats(): PrinterCardStatType[] {
return [
PrinterCardStatType.Status,
PrinterCardStatType.ETA,
PrinterCardStatType.Elapsed,
PrinterCardStatType.Remaining,
];
}
export function getDefaultFDMMonitoredStats(): PrinterCardStatType[] {
return [
...getDefaultMonitoredStats(),
PrinterCardStatType.HotendCurrent,
PrinterCardStatType.BedCurrent,
PrinterCardStatType.HotendTarget,
PrinterCardStatType.BedTarget,
];
}
export function getPanelBasicMonitoredStats(): PrinterCardStatType[] {
return [
...getDefaultMonitoredStats(),
PrinterCardStatType.PrinterOnline,
PrinterCardStatType.Availability,
PrinterCardStatType.ProjectName,
PrinterCardStatType.CurrentLayer,
];
}
export function getPanelFDMMonitoredStats(): PrinterCardStatType[] {
return [
...getDefaultFDMMonitoredStats(),
PrinterCardStatType.PrinterOnline,
PrinterCardStatType.Availability,
PrinterCardStatType.ProjectName,
PrinterCardStatType.CurrentLayer,
];
}
export function getPanelACEMonitoredStats(): PrinterCardStatType[] {
return [
...getPanelFDMMonitoredStats(),
PrinterCardStatType.DryingStatus,
PrinterCardStatType.DryingTime,
];
}
export function getDefaultCardConfig(): AnycubicCardConfig {
return {
vertical: false,
round: false,
use_24hr: true,
temperatureUnit: TemperatureUnit.C,
monitoredStats: getDefaultMonitoredStats(),
scaleFactor: 1,
slotColors: [],
showSettingsButton: false,
alwaysShow: false,
};
}
// eslint-disable-next-line
export function undefinedDefault(value: any, defaultValue: any): any {
return typeof value === "undefined" ? defaultValue : value;
}
export function speedModesFromStateObj(
speedModeState: AnycubicSpeedModeEntity,
): AnycubicSpeedModes {
const speedModeAttr: AnycubicSpeedMode[] =
(speedModeState.attributes.available_modes as
| AnycubicSpeedMode[]
| undefined) ?? [];
return speedModeAttr.reduce(
(modes, mode) => ({ ...modes, [mode.mode]: mode.description }),
{},
);
}
export function materialTypeFromString(
material_type?: string,
): AnycubicMaterialType | undefined {
return material_type &&
(Object.values(AnycubicMaterialType) as string[]).includes(material_type)
? AnycubicMaterialType[material_type.toUpperCase() as AnycubicMaterialType]
: undefined;
}

View File

@@ -1,90 +0,0 @@
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/*
* This was pulled AND MODIFIED from the URL below as
* LitElements does not prevent the same element from
* being registered more than once causing errors.
* https://github.com/lit/lit-element/blob/master/src/lib/decorators.ts
*
* Idea: https://github.com/lit/lit-element/issues/207#issuecomment-1150057355
*/
interface Constructor<T> {
// tslint:disable-next-line:no-any
new (...args: any[]): T;
}
// From the TC39 Decorators proposal
interface ClassElement {
kind: "field" | "method";
key: PropertyKey;
placement: "static" | "prototype" | "own";
initializer?: Function;
extras?: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
descriptor?: PropertyDescriptor;
}
// From the TC39 Decorators proposal
interface ClassDescriptor {
kind: "class";
elements: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
}
const legacyCustomElement = (
tagName: string,
clazz: Constructor<HTMLElement>,
): any => {
if (window.customElements.get(tagName)) {
return clazz as any;
}
window.customElements.define(tagName, clazz);
// Cast as any because TS doesn't recognize the return type as being a
// subtype of the decorated class when clazz is typed as
// `Constructor<HTMLElement>` for some reason.
// `Constructor<HTMLElement>` is helpful to make sure the decorator is
// applied to elements however.
return clazz as any;
};
const standardCustomElement = (
tagName: string,
descriptor: ClassDescriptor,
): any => {
const { kind, elements } = descriptor;
return {
kind,
elements,
// This callback is called once the class is otherwise fully defined
finisher(clazz: Constructor<HTMLElement>): void {
if (window.customElements.get(tagName)) {
return;
}
window.customElements.define(tagName, clazz);
},
};
};
/**
* Class decorator factory that defines the decorated class as a custom element.
*
* ```
* @customElement('my-element')
* class MyElement {
* render() {
* return html``;
* }
* }
* ```
* @category Decorator
* @param tagName The name of the custom element to define.
*/
export const customElementIfUndef =
(tagName: string): any =>
(classOrDescriptor: Constructor<HTMLElement> | ClassDescriptor): any =>
typeof classOrDescriptor === "function"
? legacyCustomElement(tagName, classOrDescriptor)
: standardCustomElement(tagName, classOrDescriptor);

View File

@@ -1,171 +0,0 @@
import { html, LitElement } from "lit";
import { Color } from "modern-color";
import { hueGradient } from "./lib.js";
import { styleMap } from "lit/directives/style-map.js";
import { classMap } from "lit/directives/class-map.js";
import { inputChannelRules } from "./css.js";
import { colorEvent } from "./lib.js";
const labelDictionary = {
r: "R (red) channel",
g: "G (green) channel",
b: "B (blue) channel",
h: "H (hue) channel",
s: "S (saturation) channel",
v: "V (value / brightness) channel",
l: "L (luminosity) channel",
a: "A (alpha / opacity) channel",
};
export class ColorInputChannel extends LitElement {
static properties = {
group: { type: String },
channel: { type: String },
color: { type: Object },
isHsl: { type: Boolean },
c: { type: Object, state: true, attribute: false },
previewGradient: { type: Object, state: true, attribute: false },
active: { type: Boolean, state: true, attribute: false },
max: { type: Number, state: true, attribute: false },
v: { type: Number, state: true, attribute: false },
};
static styles = inputChannelRules;
clickPreview(e) {
const w = 128;
const x = Math.max(0, Math.min(e.offsetX, w));
let v = Math.round((x / 128) * this.max);
if (this.channel === "a") {
v = Number((x / 127).toFixed(2));
}
this.valueChange(null, v);
this.setActive(false);
}
valueChange = (e, val = null) => {
val = val ?? Number(this.renderRoot.querySelector("input").value);
if (this.channel === "a") {
val /= 100;
}
this.c[this.channel] = val;
const c = Color.parse(this.c);
if (this.group !== "rgb") {
c.hsx = this.c;
}
this.c =
this.group === "rgb"
? this.color.rgbObj
: this.isHsl
? this.color.hsl
: this.color.hsv;
colorEvent(this.renderRoot, c);
};
setActive(active) {
this.active = active;
if (active) {
this.renderRoot.querySelector("input").select();
}
}
constructor() {
super();
}
setPreviewGradient() {
let c;
if (this.group === "rgb") {
c = this.color.rgbObj;
} else {
if (this.color.hsx) {
c = this.color.hsx;
} else {
c = this.isHsl ? this.color.hsl : this.color.hsv;
}
}
this.c = c;
const g = this.group;
const ch = this.channel;
const isAlpha = ch === "a";
this.v = c[ch];
if (isAlpha) {
this.v *= 100;
}
let max = 255;
let minC, maxC;
if (g !== "rgb" || ch === "a") {
if (ch === "h") {
max = this.max = 359;
this.previewGradient = {
"--preview": `linear-gradient(90deg, ${hueGradient(24, c)})`,
"--pct": `${100 * (c.h / max)}%`,
};
return;
} else if (isAlpha) {
max = 1;
} else {
max = 100;
}
}
this.max = max;
minC = { ...c };
maxC = minC;
minC[this.channel] = 0;
minC = Color.parse(minC);
maxC[this.channel] = max;
maxC = Color.parse(maxC);
if (this.channel === "l") {
const midC = { ...c };
midC.l = 50;
this.previewGradient = {
"--preview": `linear-gradient(90deg, ${minC.hex}, ${Color.parse(midC).hex}, ${maxC.hex})`,
"--pct": `${100 * (c[this.channel] / max)}%`,
};
} else {
this.previewGradient = {
"--preview": `linear-gradient(90deg, ${isAlpha ? minC.css : minC.hex}, ${isAlpha ? maxC.css : maxC.hex})`,
"--pct": `${100 * (c[this.channel] / max)}%`,
};
}
}
willUpdate(props) {
this.setPreviewGradient();
}
render() {
const chex =
this.channel === "a"
? html`<div class="transparent-checks"></div>`
: null;
const max = this.channel === "a" ? 100 : this.max;
return html` <div class="${classMap({ active: this.active })}">
<label for="channel_${this.ch}">${this.channel.toUpperCase()}</label>
<input
id="channel_${this.ch}"
aria-label="${labelDictionary[this.channel]}"
class="form-control"
.value="${Math.round(this.v)}"
type="number"
min="0"
max="${max}"
@input="${this.valueChange}"
@focus="${() => this.setActive(true)}"
@blur="${() => this.setActive(false)}"
/>
<div
class="preview-bar"
style="${styleMap(this.previewGradient)}"
@mousedown="${this.clickPreview}"
>
<div class="pct"></div>
${chex}
</div>
</div>`;
}
}
if (!customElements.get("color-input-channel")) {
customElements.define("color-input-channel", ColorInputChannel);
}

View File

@@ -1,299 +0,0 @@
// noinspection ES6UnusedImports
import { css, html, LitElement } from "lit";
import { Color, namedColors } from "modern-color";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
// todo: understand why eslint thinks these are unused - they are dependencies
import { HueBar } from "./HueBar.js";
import { ColorInputChannel } from "./ColorInputChannel.js";
import { HSLCanvas } from "./HSLCanvas.js";
import {
focusedFormControl,
formControl,
root,
transparentChex,
} from "./css.js";
import { colorEvent, copy } from "./lib.js";
import { LitMovable } from "../movable/LitMovable";
//todo: light/dark mode + get decorators working without typescript
export class ColorPicker extends LitElement {
static properties = {
color: { type: Object, state: true, attribute: false },
hex: { type: String, state: true, attribute: false },
value: { type: String },
isHsl: { type: Boolean, state: true, attribute: false },
copied: { type: String },
debounceMode: { type: Boolean },
buttonDisabled: { attribute: "button-disabled", type: Boolean },
};
static styles = root;
_color;
constructor() {
super();
this._color = Color.parse(namedColors.slateblue);
this.isHsl = true;
this.buttonDisabled = false;
}
firstUpdated(props) {
this.debounceMode = false;
if (props.has("value")) {
this.color = Color.parse(this.value);
}
}
get color() {
return this._color;
}
set color(c) {
c = c.hsx ? c : c.rgba ? Color.parse(...c.rgba) : Color.parse(c);
if (c) {
this.hex = c.hex;
this._color = c;
colorEvent(this.renderRoot, c, "colorchanged");
}
}
updateColor({ detail: { color } }) {
this.color = color;
}
setColor(e) {
const cs = this.renderRoot.querySelector("input#hex").value;
const c = Color.parse(cs);
if (c) {
this.color = c;
} else {
console.log(`ignored unparsable input: ${cs}`);
}
}
setHue({ detail: { h } }) {
let { s, l, a } = this.color.hsl;
if (a === 1) {
a = undefined;
}
this.color = { h, s, l, a };
}
setHsl(hsl) {
this.isHsl = hsl;
}
okColor() {
colorEvent(this.renderRoot, this.color, "colorpicked");
}
showCopyDialog() {
this.copied = null;
this.dlg = this.dlg ?? this.renderRoot.querySelector("dialog");
if (this.dlg.open) {
this.dlg.classList.remove("open");
return this.dlg.close();
}
this.dlg.show();
this.dlg.classList.add("open");
}
clipboard(f) {
const s = this.color.toString(f);
window.navigator.clipboard.writeText(s).then(() => {
this.hideCopyDialog(s);
});
}
hideCopyDialog(copyText) {
if (copyText) {
this.copied = copyText;
setTimeout(() => this.dlg.classList.remove("open"), 400);
setTimeout(() => this.hideCopyDialog(), 1200);
return;
}
this.dlg.classList.remove("open");
this.dlg.close();
this.copied = null;
}
setSliding({ detail }) {
this.debounceMode = detail.sliding;
}
render() {
const hslChannels = this.isHsl ? ["h", "s", "l"] : ["h", "s", "v"];
const hsvClass = { button: true, active: !this.isHsl, l: true };
const hslClass = { button: true, active: this.isHsl, r: true };
const swatchBg = { backgroundColor: this.color };
const hideCopied = this.copied
? { textAlign: "center", display: "block" }
: { display: "none" };
const debounceMode = this.debounceMode;
return html` <div class="outer">
<hue-bar
@sliding-hue="${this.setSliding}"
hue="${this.color.hsx ? this.color.hsx.h : this.color.hsl.h}"
@hue-update="${this.setHue}"
.color="${this.color}"
></hue-bar>
<div class="d-flex">
<div class="col w-30">
${["r", "g", "b", "a"].map(
(c) => html`
<color-input-channel
group="rgb"
channel="${c}"
isHsl="${this.isHsl}"
.color="${this.color}"
@color-update="${this.updateColor}"
/>
`,
)}
<div class="hex">
<dialog @blur="${() => this.hideCopyDialog()}" tabindex="0">
<sub class="copied" style="${styleMap(hideCopied)}"
>copied <em>${this.copied}</em></sub
>
${this.copied
? html``
: html`
<a
class="copy-item"
@click=${(e) => this.clipboard("hex", e)}
id="copyHex"
>
<input
class="form-control"
disabled="disabled"
value="${this.color.hex}"
/>
<button
title="Copy HEX String"
class="button"
tabindex="0"
>
${copy}
</button>
</a>
<a
class="copy-item"
@click=${(e) => this.clipboard("css", e)}
id="copyRgb"
>
<input
class="form-control"
disabled="disabled"
value="${this.color.css}"
/>
<button
title="Copy RGB String"
class="button"
tabindex="0"
>
${copy}
</button>
</a>
<a
class="copy-item"
id="copyHsl"
@click=${(e) =>
this.clipboard(
this.color.alpha < 1 ? "hsla" : "hsl",
e,
)}
>
<input
class="form-control"
disabled="disabled"
value="${this.color.toString(
this.color.alpha < 1 ? "hsla" : "hsl",
)}"
/>
<button
title="Copy HSL String"
class="button"
tabindex="0"
>
${copy}
</button>
</a>
`}
</dialog>
<label for="hex">#</label>
<input
aria-label="Hexadecimal value (editable - accepts any valid color string)"
@input="${this.setColor}"
class="form-control"
id="hex"
placeholder="Set color"
value="${this.hex}"
/><a
title="Show copy to clipboard menu"
@click="${this.showCopyDialog}"
class="button copy"
>
${copy}
<span>&#11205;</span>
</a>
</div>
</div>
<div class="col w-30">
${hslChannels.map(
(c) => html`
<color-input-channel
group="hsl"
channel="${c}"
.isHsl="${this.isHsl}"
.color="${this.color}"
@color-update="${this.updateColor}"
/>
`,
)}
<div class="hsl-mode">
<a
title="Use hue / saturation / value (brightness) mode"
class="${classMap(hsvClass)}"
@click="${() => this.setHsl(false)}"
>HSV</a
><a
title="Use hue / saturation / luminosity mode"
class="${classMap(hslClass)}"
@click="${() => this.setHsl(true)}"
>HSL</a
>
</div>
</div>
<div class="w-40">
<hsl-canvas
.debounceMode="${debounceMode}"
size="${160}"
.isHsl="${this.isHsl}"
.color="${this.color}"
@color-update="${this.updateColor}"
></hsl-canvas>
<div class="ok">
<a
class="button"
.disabled=${this.buttonDisabled}
@click="${this.okColor}"
>OK
<span class="swatch">
<span style="${styleMap(swatchBg)}"></span>
<span class="checky"></span>
</span>
</a>
</div>
</div>
</div>
</div>`;
}
}
if (!window.customElements.get("color-picker")) {
window.customElements.define("color-picker", ColorPicker);
}

View File

@@ -1,169 +0,0 @@
import { styleMap } from "lit/directives/style-map.js";
import { LitElement, html, css } from "lit";
import { Color } from "modern-color";
import { colorEvent } from "./lib.js";
export class HSLCanvas extends LitElement {
static properties = {
color: { type: Object },
isHsl: { type: Boolean },
size: { type: Number },
debounceMode: { type: Boolean },
ctx: { type: Object, state: true, attribute: false },
hsw: { type: Object, state: true, attribute: false },
circlePos: { type: Object, state: true, attribute: false },
};
static styles = css`
:host .outer {
position: absolute;
top: 0;
right: 0;
}
:host .outer canvas {
height: inherit;
width: inherit;
cursor: pointer;
}
:host .circle {
height: 12px;
width: 12px;
border: solid 2px #eee;
border-radius: 50%;
box-shadow:
0 0 3px #000,
inset 0 0 1px #fff;
position: absolute;
margin: -8px;
mix-blend-mode: difference;
}
`;
constructor() {
super();
this.isHsl = true;
this.circlePos = { top: 0, left: 0, bounds: { x: "", y: "" } };
this.size = 160;
}
setColor(c) {
//this.color = c;
colorEvent(this.renderRoot, c);
}
setCircleCss(x, y) {
const left = `${x}`;
const top = `${y}`;
const bounds = { x: `0, ${this.size}`, y: `0,${this.size}` };
//let bounds = {x: `${-x}, ${this.size-x}`,y:`${-y},${this.size-y}`}
this.circlePos = { top, left, bounds };
}
pickCoord({ offsetX, offsetY }) {
const x = offsetX;
const y = offsetY;
const { size, hsw, isHsl, color } = this;
let w = (size - y) / size;
w = Math.round(w * 100);
const sat = Math.round((x / size) * 100);
const hsx = { h: hsw.h, s: sat, [isHsl ? "l" : "v"]: w };
const c = isHsl ? Color.fromHsl(hsx) : Color.fromHsv(hsx);
this.setCircleCss(x, y);
c.a = color.alpha;
c.hsx = hsx;
c.fromHSLCanvas = true;
this.setColor(c);
}
debouncePaintDetail(hsx) {
clearTimeout(this.bouncer);
this.bouncer = setTimeout(() => this.paintHSL(hsx, true), 50);
this.paintHSL(hsx, false);
}
// todo: test assumption that this perf lag (lit warning)
// is ok due to rendering canvas post update
paintHSL(hsx, detail = null) {
if (this.debounceMode && detail === null) {
// enable rapid painting in lower res
return this.debouncePaintDetail(hsx);
}
const { ctx, color, isHsl, size } = this;
if (!ctx) {
return;
}
//console.time('paint'+detail)
const clr = color;
hsx = (hsx ?? isHsl) ? clr.hsl : clr.hsv; // hue-sat-whatever
hsx.w = isHsl ? hsx.l : hsx.v;
const { h, s, w } = hsx;
const hsw = (this.hsw = { h, s, w });
const scale = size / 100;
const fillHsl = (h, s, l) => `hsl(${h}, ${s}%, ${100 - l}%)`;
const fillHsv = (h, s, v) => Color.fromHsv({ h, s, v: 100 - v }).hex;
const fill = isHsl ? fillHsl : fillHsv;
const incr = detail === false ? 4 : 1; //rapid painting during hue slider ops
for (let s = 0; s < 100; s += incr) {
for (let w = 0; w < 100; w += incr) {
ctx.fillStyle = fill(h, s, w);
ctx.fillRect(s, w, s + incr, w + incr);
}
}
this.setCircleCss(hsw.s * scale, size - hsx.w * scale);
//console.timeEnd('paint'+detail)
}
willUpdate(props) {
if (props.has("color") || props.has("isHsl")) {
if (this.color?.hsx) {
if (this.color.fromHSLCanvas) {
delete this.color.fromHSLCanvas; //avoid extra paint job
return;
}
return this.paintHSL(this.color.hsx);
}
this.paintHSL();
}
}
firstUpdated(props) {
const canvas = this.renderRoot.querySelector("canvas");
this.ctx = canvas.getContext("2d");
this.paintHSL();
}
circleMove({ posTop: offsetY, posLeft: offsetX }) {
this.pickCoord({ offsetX, offsetY });
}
render() {
const hw = { height: this.size + "p", width: this.size + "px" };
const { top, left, bounds } = this.circlePos;
return html` <div
class="outer"
@click="${this.pickCoord}"
style="${styleMap(hw)}"
>
<canvas height="100" width="100"></canvas>
<lit-movable
boundsX="${bounds.x}"
boundsY="${bounds.y}"
posTop="${top}"
posLeft="${left}"
.onmove="${(e) => this.circleMove(e)}"
>
<div class="circle"></div>
</lit-movable>
</div>`;
}
}
if (!customElements.get("hsl-canvas")) {
customElements.define("hsl-canvas", HSLCanvas);
}

View File

@@ -1,122 +0,0 @@
import { LitElement, html, css, unsafeCSS } from "lit";
import { Color } from "modern-color";
import { styleMap } from "lit/directives/style-map.js";
import { colorEvent, hueGradient } from "./lib.js";
export class HueBar extends LitElement {
static properties = {
hue: { type: Number },
color: { type: Object },
gradient: { type: String, attribute: false },
sliderStyle: { type: String, attribute: false },
sliderBounds: { type: Object },
width: { type: Number, attribute: false },
};
static styles = css`
:host > div {
display: block;
width: ${unsafeCSS(this.width)}px;
height: 15px;
cursor: pointer;
position: relative;
}
:host .slider {
position: absolute;
top: -1px;
height: 17px;
width: 8px;
margin-left: -4px;
box-shadow:
0 0 3px #111,
inset 0 0 2px white;
}
`;
constructor() {
super();
this.gradient = {
backgroundImage: `linear-gradient(90deg, ${hueGradient(24)})`,
};
this.width = 400;
this.sliderStyle = { display: "none" };
}
firstUpdated() {
const me = this.renderRoot.querySelector("lit-movable");
me.onmovestart = () => {
colorEvent(this.renderRoot, { sliding: true }, "sliding-hue");
};
me.onmoveend = () => {
colorEvent(this.renderRoot, { sliding: false }, "sliding-hue");
};
me.onmove = ({ posLeft }) => this.selectHue({ offsetX: posLeft });
this.sliderStyle = this.sliderCss(this.hue);
}
get sliderBounds() {
const r = this.width / 360;
const posLeft = Number(this.hue) * r;
const min = 0 - posLeft;
const max = this.width - posLeft;
return { min, max, posLeft };
}
get sliderCss() {
return (h) => {
if (this.color.hsx) {
h = this.color.hsx.h;
}
if (h === undefined) {
h = this.color.hsl.h;
}
const color = Color.parse({ h, s: 100, l: 50 });
return { backgroundColor: color.css };
};
}
willUpdate(props) {
const h = props.get("hue");
if (h && isFinite(this.hue)) {
if (this.color?.hsx) {
return; // console.log({hueBarIgnored: this.color.hsx});
}
const hue = this.hue;
this.sliderStyle = this.sliderCss(hue);
}
}
selectHue(e) {
const r = 360 / this.width;
const l = e.offsetX;
const h = Math.max(0, Math.min(359, Math.round(l * r)));
const target = this.renderRoot.querySelector("a");
const event = new CustomEvent("hue-update", {
bubbles: true,
composed: true,
detail: { h },
});
target.dispatchEvent(event);
this.sliderStyle = this.sliderCss(h);
}
render() {
return html` <div
style=${styleMap(this.gradient)}
class="bar"
@click="${this.selectHue}"
>
<lit-movable
horizontal="${this.sliderBounds.min}, ${this.sliderBounds.max}"
posLeft="${this.sliderBounds.posLeft}"
>
<a class="slider" style=${styleMap(this.sliderCss(this.h))}></a>
</lit-movable>
</div>`;
}
}
if (!customElements.get("hue-bar")) {
customElements.define("hue-bar", HueBar);
}

View File

@@ -1,363 +0,0 @@
import { css } from "lit";
export const transparentChex = css`
height: 100%;
width: 100%;
position: absolute;
z-index: -1;
background: linear-gradient(
45deg,
rgba(0, 0, 0, 0.125) 25%,
transparent 0,
transparent 75%,
rgba(0, 0, 0, 0.125) 0,
rgba(0, 0, 0, 0.125) 0
),
linear-gradient(
45deg,
rgba(0, 0, 0, 0.125) 25%,
transparent 0,
transparent 75%,
rgba(0, 0, 0, 0.125) 0,
rgba(0, 0, 0, 0.125) 0
),
#fff;
background-repeat: repeat, repeat;
background-position:
0 0,
6px 6px;
background-size:
12px 12px,
12px 12px;
`;
export const formControl = css`
display: inline-block;
width: 69px;
padding: 0.325rem 0.5rem;
font-size: 0.9rem;
font-weight: 400;
line-height: 1.5;
color: var(--input-color);
appearance: none;
background-color: var(--input-bg);
background-clip: padding-box;
border: 1px solid var(--form-border-color);
border-radius: 3px;
transition:
border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;
`;
export const focusedFormControl = css`
color: var(--input-active-color);
background-color: var(--input-active-bg);
border-color: var(--input-active-border-color);
outline: 0;
box-shadow: var(--input-active-box-shadow);
`;
export const root = css`
:host {
--font-fam: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
"Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bg-color: rgb(30 41 59);
--label-color: #ccc;
--form-border-color: #495057;
--input-active-border-color: #86b7fe;
--input-bg: #020617;
--input-active-bg: #4682b4;
--input-color: #ccc;
--input-active-color: #333;
--input-active-box-shadow: 0 2px 5px #ccc;
--button-active-bg: #0c5b9d;
--button-active-color: white;
--outer-box-shadow: 0 4px 12px #111;
}
:host > .outer {
position: relative;
background-color: var(--bg-color);
height: 250px;
width: 400px;
display: block;
padding: 10px;
margin: 10px;
box-shadow: var(--outer-box-shadow);
}
.d-flex {
display: flex;
width: 100%;
margin-top: 15px;
}
.w-30 {
width: 30%;
}
.w-40 {
width: 40%;
position: relative;
height: 210px;
}
:host .form-control {
${formControl}
}
:host .form-control:focus {
${focusedFormControl}
}
:host label {
width: 12px;
display: inline-block;
color: var(--label-color);
font-family: var(--font-fam);
}
:host .hsl-mode {
padding-left: 16px;
margin-top: 18px;
}
:host .button {
padding: 0.325rem 0.5rem;
background-color: var(--input-bg);
border: 1px solid var(--form-border-color);
font-family: var(--font-fam);
color: var(--input-color);
cursor: pointer;
font-size: 0.9rem;
}
:host div.hex {
margin-top: 27px;
white-space: nowrap;
position: relative;
}
:host dialog {
opacity: 0;
width: 177px;
position: absolute;
bottom: 30px;
left: 0px;
z-index: 3;
border: 1px solid transparent;
outline: transparent;
box-shadow: var(--outer-box-shadow);
background-color: var(--input-bg);
transition: opacity 0.3s;
}
:host dialog.open {
opacity: 1;
}
:host dialog * {
color: var(--input-color);
}
:host dialog a.copy-item {
margin-bottom: 5px;
white-space: nowrap;
display: block;
width: 180px;
cursor: pointer;
}
:host dialog input.form-control {
font-size: 12px;
display: inline-block;
vertical-align: middle;
width: 132px;
padding-bottom: 2px;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
pointer-events: none;
}
:host dialog button.button {
display: inline-block;
vertical-align: middle;
margin-left: -5px;
font-size: 12px;
height: 27px;
width: 27px;
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
box-sizing: border-box;
overflow: hidden;
outline: none;
background-color: transparent;
}
:host dialog a.copy-item:hover .button,
:host dialog a.copy-item:hover input.form-control,
:host dialog a.copy-item:hover path {
color: var(--button-active-color);
background-color: var(--button-active-bg);
fill: var(--button-active-color);
cursor: pointer;
}
:host dialog .button svg {
height: 15px;
width: 15px;
margin-left: -3px;
}
:host div.hex input {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
vertical-align: middle;
display: inline-block;
}
:host .button.copy {
padding: 8px 6px 5px 5px;
position: relative;
position: relative;
border-left: 0;
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
height: 34px;
display: inline-block;
box-sizing: border-box;
overflow: hidden;
vertical-align: middle;
}
:host .button.copy svg {
height: 16px;
width: 15px;
margin-right: -2px;
}
:host .button.copy span {
font-size: 10px;
position: relative;
top: -3px;
}
:host a.button.l {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
:host a.button.r {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-left: none;
}
:host a.button.active {
color: #eee;
background-color: var(--button-active-bg);
cursor: default;
}
:host .ok {
position: absolute;
bottom: 0;
right: 0;
}
:host .ok a {
border-radius: 3px;
padding: 6px 12px;
}
:host .swatch {
height: 14px;
width: 14px;
display: inline-block;
position: relative;
top: 2px;
margin-left: 3px;
}
:host .swatch span {
position: absolute;
z-index: 1;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
:host .swatch span.checky {
${transparentChex}
z-index: 0;
}
`;
export const inputChannelRules = css`
:host > div {
margin-bottom: 8px;
display: block;
position: relative;
}
:host label {
width: 12px;
display: inline-block;
color: var(--label-color);
font-family: var(--font-fam);
}
:host .form-control {
${formControl}
}
:host .form-control:focus {
${focusedFormControl}
}
:host .preview-bar {
height: 4px;
width: 85.5px;
position: absolute;
bottom: 0px;
right: 17.5px;
--pct: 0;
pointer-events: none;
z-index: 2;
}
:host .preview-bar:after {
position: absolute;
content: "";
background-image: var(--preview);
background-color: transparent;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
box-shadow: inset 0 -1px 1px var(--form-border-color);
height: 100%;
width: 100%;
}
:host > div.active .preview-bar {
width: 128px;
bottom: -23px;
right: -9px;
height: 10px;
border: 8px solid var(--input-bg);
box-shadow: var(--input-active-box-shadow);
pointer-events: all;
z-index: 2;
cursor: pointer;
}
:host > div.active .preview-bar:after {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
:host .preview-bar .pct {
bottom: -3px;
margin-top: -0.75px;
position: absolute;
width: 3px;
height: 11px;
background: 0 0;
left: var(--pct);
display: inline-block;
z-index: 3;
pointer-events: none;
}
:host .preview-bar .pct:before {
content: "";
height: 7px;
width: 5px;
position: absolute;
left: -2.5px;
top: 2.5px;
background-color: #fff;
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
:host .active .preview-bar .pct:before {
width: 7px;
height: 11px;
left: -3.5px;
top: -1px;
}
:host .transparent-checks {
${transparentChex}
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
:host div.active .transparent-checks {
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}
`;

View File

@@ -1,61 +0,0 @@
import { Color } from "modern-color";
import { html } from "lit";
export const colorEvent = (target, color, name = "color-update") => {
const detail = name.includes("color") ? { color } : color;
const event = new CustomEvent(name, {
bubbles: true,
composed: true,
detail,
});
target.dispatchEvent(event);
};
export const hueGradient = (gran = 3, hsx) => {
//todo: update to take optional hsx(v/l) vals and compose
let h = 0;
let s = 100;
let l = 50;
let v = null;
let isHsv = false;
if (hsx) {
s = hsx.s;
if (hsx.hasOwnProperty("v")) {
v = hsx.v;
l = null;
isHsv = true;
} else {
l = hsx.l;
}
}
const stops = [];
let color, pos;
const cs = (color, pos) => `${color.css} ${(pos * 100).toFixed(1)}%`;
while (h < 360) {
color = Color.parse(isHsv ? { h, s, v } : { h, s, l });
pos = h / 360;
stops.push(cs(color, pos));
h += gran;
}
h = 359;
color = Color.parse(isHsv ? { h, s, v } : { h, s, l });
pos = 1;
stops.push(cs(color, pos));
return stops.join(", ");
};
export const copy = html`<svg
stroke="currentColor"
fill="none"
stroke-width="0"
viewBox="0 0 24 24"
>
<path d="M13 7H7V5H13V7Z" fill="currentColor"></path>
<path d="M13 11H7V9H13V11Z" fill="currentColor"></path>
<path d="M7 15H13V13H7V15Z" fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 19V1H17V5H21V23H7V19H3ZM15 17V3H5V17H15ZM17 7V19H9V21H19V7H17Z"
fill="currentColor"
></path>
</svg>`;

View File

@@ -1,478 +0,0 @@
import { LitElement, html } from "lit";
const pxVal = (v) =>
isFinite(v) ? Number(v) : Number(v.replace(/[^0-9.\-]/g, ""));
const zeroIfNaN = (v) => {
v = Number(v);
if (isNaN(v) || [undefined, null].includes(v)) {
v = 0;
}
return v;
};
class Coord {
constructor(x, y) {
this.x = zeroIfNaN(x);
this.y = zeroIfNaN(y);
}
static fromPointerEvent(event) {
const { pageX, pageY } = event;
return new Coord(pageX, pageY);
}
static fromElementStyle(el) {
const x = pxVal(el.style.left ?? 0);
const y = pxVal(el.style.top ?? 0);
return new Coord(x, y);
}
static fromObject({ x, y }) {
return new Coord(x, y);
}
get top() {
return this.y;
}
set top(v) {
this.y = v;
}
get left() {
return this.x;
}
set left(v) {
this.x = v;
}
}
const getClickOffset = (event) => {
const coords = Coord.fromPointerEvent(event);
const off = event.target.getBoundingClientRect();
const x = coords.x - (off.left + document.body.scrollLeft);
const y = coords.y - (off.top + document.body.scrollTop);
return new Coord(x, y);
};
class MoveBounds {
constructor(min = -Infinity, max = Infinity) {
this.min = min;
this.max = max;
this.attr = "";
}
get constrained() {
return this.min === this.max;
}
get unconstrained() {
return this.min === -Infinity && this.max === Infinity;
}
static fromString(s = null, offset = 0) {
if (!s) {
return new MoveBounds();
}
if (s === "null") {
return new MoveBounds(0, 0);
}
const [min, max] = s.split(",").map((n) => Number(n.trim()) + offset);
const bounds = new MoveBounds(min, max);
bounds.attr = s;
return bounds;
}
}
/**
* @attr {number} posTop - Represents the offsetTop value (reflected). When set, will set the initial style.top value. Updates with move events
* @attr {number} posLeft - Represents the offsetLeft value (reflected). When set, will set the initial style.top value. Updates with move events
* @attr {string} targetSelector - A selector to select the element that will move. Defaults to the lit-movable (this) element, but useful when for example you want to allow a modal header to respond to pointer events but you want the entire modal to move.
* @attr {string} boundsX - Set to boundsX="min,max" to restrict movement along the x axis
* @attr {string} boundsY - Set to boundsY="min,max" to restrict movement along the y axis
* @attr {string} vertical - Will constrain horizontal (x) movement completely and allow vertical (y) movement between the specified values
* @attr {string} horizontal - Will constrain vertical (y) movement completely and allow horizontal (x) movement between the specified values
* @attr {number} grid - Snaps movement to nearest grid position. Initial element position represents the 0,0 position. Movement snapped to the provided value increment
* @attr {boolean} shiftBehavior - When enabled, holding the shift key will coerce movement to perpendicular coordinates only.
* @attr {boolean} disabled - Disables movement behavior
* @attr {boolean} eventsOnly - Only fires movement events, but will not move the element
*
* @slot - default/unnamed slot
*
* @prop {object} target - The target element that will move
* @prop {object} bounds - Computed from the specified boundsX, boundsY attributes. Represents the runtime movement constraints if any
*
* @fires onmovestart - Initial state when user initiates a move operation (onpointerdown). Bind syntax: element.onmovestart=(state)=>console.log(state).
* @fires onmove - Fires continuously after onpointerdown until document.onpointerup event. Bind syntax: element.onmove=(state)=>console.log(state).
* @fires onmovestart - Final state when user completes a move operation (document.onpointerup). Bind syntax: element.onmoveend=(state)=>console.log(state).
*
* @event {CustomEvent} movestart - Initial state when user initiates a move operation (onpointerdown). * Bind with element.addEventListener('movestart', ({detail}) => console.log({moveState:detail}))
* @event {CustomEvent} move - Fires continuously after onpointerdown until document.onpointerup event. Bind with element.addEventListener('move', ({detail}) => console.log({moveState:detail}))
* @event {CustomEvent} moveend - Final state when user completes a move operation (document.onpointerup). Bind with element.addEventListener('moveend', ({detail}) => console.log({moveState:detail}))
*
* @summary A Lit 3 wrapper web component that can enable robustly customizable element move operations and expose rich state data.
*
* @tag lit-movable
*/
export class LitMovable extends LitElement {
_target;
_targetSelector = null;
_boundsX = new MoveBounds();
_boundsY = new MoveBounds();
isMoving = false;
moveState = {};
_vertical = null;
_horizontal = null;
_posTop = null;
_posLeft = null;
_grid = 1;
pointerId;
constructor() {
super();
}
get vertical() {
return this._vertical;
}
set vertical(v) {
this.boundsY = v;
this.boundsX = "null";
this._vertical = v;
}
get horizontal() {
return this._horizontal;
}
set horizontal(v) {
this.boundsX = v;
this.boundsY = "null";
this._horizontal = v;
}
set posTop(v) {
v = Number(v);
this._posTop = v;
if (this.target) {
this.target.style.top = v + "px";
}
}
get posTop() {
return this._posTop;
}
set posLeft(v) {
v = Number(v);
this._posLeft = v;
if (this.target) {
this.target.style.left = v + "px";
}
}
get posLeft() {
return this._posLeft;
}
get grid() {
return this._grid;
}
set grid(v) {
if (v > 0 && v < Infinity) {
this._grid = v;
} else {
this._grid = 1;
}
}
get bounds() {
return {
left: this._boundsX,
top: this._boundsY,
};
}
set targetSelector(v) {
this._targetSelector = v;
this._retryTarget = document.querySelector(v) === null;
this._target = document.querySelector(v);
}
get targetSelector() {
return this._targetSelector;
}
get target() {
return this._target ?? this;
}
set target(v) {
this._target = v;
}
get boundsX() {
return this._boundsX;
}
set boundsX(v) {
this._boundsX = MoveBounds.fromString(
v,
pxVal(this.target?.style.left ?? 0),
);
this.bounds.left = this._boundsX;
}
get boundsY() {
return this._boundsY;
}
set boundsY(v) {
this._boundsY = MoveBounds.fromString(
v,
pxVal(this.target?.style.top ?? 0),
);
//let offsetTop =
this.bounds.top = this._boundsY;
}
static properties = {
//set the left/top position
// defaults to element.offsetTop /offsetLeft
posLeft: { type: Number },
posTop: { type: Number },
// target element that moves - defaults to root element
target: { type: Object, attribute: false, state: true },
// selector that will set the target element that will move
targetSelector: { type: String },
// object (left:boundsX,top:boundsY)
bounds: { type: Object, attribute: false, state: true },
// Both x and y default to -Infinity,Infinity.
// Set to boundsX="min,max" ([0,0] to restrict the axis)
// these are attribute string setters meant for declarative
// element attribute setting
boundsX: { type: String },
boundsY: { type: String },
// vertical="min,max" - constrain movement to y axis within min and max numbers provided.
// automatically disables horizontal movement
vertical: { type: String },
// horizontal="min,max" - constrain movement to x axis within min and max provided.
// automatically disables vertical movement
horizontal: { type: String },
//defaults to 1. snap to grid size in pixels.
grid: { type: Number },
// set to true enables shift key to constrain movement to either
// x or y axis (whichever is greater).
// Setting any bounds option automatically disables shift key behavior.
shiftBehavior: { type: Boolean },
//disables moving
disabled: { type: Boolean },
// advanced mode: Does not move the element, but fires
// events so you can pass to your own handler
eventsOnly: { type: Boolean },
listening: { type: Boolean },
onmovestart: { type: Object },
onmoveend: { type: Object },
onmove: { type: Object },
};
firstUpdated(props) {
if (this._retryTarget) {
// element wasn't loaded
this.target = document.querySelector(this.targetSelector);
}
const { bounds, target, posTop, posLeft } = this;
const {
offsetLeft,
offsetTop,
style: { left, top },
} = this.target;
target.classList.add("--movable-base");
this.renderRoot.addEventListener("pointerdown", (e) => this.pointerdown(e));
target.style.position = "absolute";
target.style.cursor = "pointer";
if (posLeft) {
target.style.left = posLeft + "px";
} else if (!left && offsetLeft) {
target.style.left = offsetLeft + "px";
if (bounds.left.constrained) {
bounds.left.min = bounds.left.max = offsetLeft;
}
}
if (posTop) {
target.style.top = posTop + "px";
} else if (!top && offsetTop) {
target.style.top = offsetTop + "px";
if (bounds.top.constrained) {
bounds.top.min = bounds.top.max = offsetTop;
}
}
}
reposition(pos) {
if (typeof pos === "object") {
const { eventsOnly, target } = this;
this.posTop = pos.top;
this.posLeft = pos.left;
if (target && !eventsOnly) {
target.style.left = pos.left + "px";
target.style.top = pos.top + "px";
}
} else {
this.isMoving = pos;
}
}
moveInit(event) {
const moveState = this.moveState;
const { target, bounds } = this;
moveState.mouseCoord = Coord.fromPointerEvent(event);
moveState.startCoord = Coord.fromElementStyle(target);
moveState.moveDist = new Coord(0, 0);
moveState.totalDist = new Coord(0, 0);
moveState.clickOffset = getClickOffset(event);
moveState.coords = Coord.fromObject(moveState.startCoord);
moveState.maxX =
isFinite(bounds.left.min) && isFinite(bounds.left.max)
? bounds.left.min + bounds.left.max
: Infinity;
moveState.maxY =
isFinite(bounds.top.min) && isFinite(bounds.top.max)
? bounds.top.min + bounds.top.max
: Infinity;
this.isMoving = true;
this.reposition(true);
this.eventBroker("movestart", event);
}
eventBroker(name, event) {
this.moveState.posTop = this.posTop;
this.moveState.posLeft = this.posLeft;
const customEvent = new CustomEvent(name, {
bubbles: true,
composed: true,
detail: { ...event, ...this.moveState, element: this },
});
this.renderRoot.dispatchEvent(customEvent);
const attrEvent = this[`on${name}`];
if (attrEvent) {
attrEvent({ ...event, ...this.moveState, me: this });
}
}
unbind(event) {
this.pointerId = null;
document.body.removeEventListener("pointermove", (e) =>
this.motionHandler(e),
);
this.moveEnd(event);
}
moveEnd(event) {
if (this.isMoving) {
//document.body.removeEventListener('pointerup', ()=>this.unbind);
this.isMoving = this.moveState.isMoving = false;
this.reposition(false);
this.eventBroker("moveend", event);
}
}
motionHandler(event) {
//onpointermove
event.stopPropagation();
const newCoord = Coord.fromPointerEvent(event);
const moveState = this.moveState;
const { grid, bounds, shiftBehavior, boundsX, boundsY } = this;
moveState.moveDist = Coord.fromObject({
x: newCoord.x - moveState.mouseCoord.x,
y: newCoord.y - moveState.mouseCoord.y,
});
moveState.mouseCoord = newCoord;
moveState.totalDist = Coord.fromObject({
x: moveState.totalDist.x + moveState.moveDist.x,
y: moveState.totalDist.y + moveState.moveDist.y,
});
moveState.coords = Coord.fromObject({
x:
Math.round(moveState.totalDist.x / grid) * grid +
moveState.startCoord.x,
y:
Math.round(moveState.totalDist.y / grid) * grid +
moveState.startCoord.y,
});
if (
shiftBehavior &&
event.shiftKey &&
boundsX.unconstrained &&
boundsY.unconstrained
) {
const { x, y } = moveState.totalDist;
if (Math.abs(x) > Math.abs(y)) {
moveState.coords.top = moveState.startCoord.y;
} else {
moveState.coords.left = moveState.startCoord.x;
}
} else {
moveState.coords.y = Math.min(
Math.max(bounds.top.min, moveState.coords.top),
bounds.top.max,
);
moveState.coords.x = Math.min(
Math.max(bounds.left.min, moveState.coords.left),
bounds.left.max,
);
}
if (isFinite(moveState.maxX)) {
moveState.pctX =
Math.max(bounds.left.min, moveState.coords.left) / moveState.maxX;
}
if (isFinite(moveState.maxY)) {
moveState.pctY =
Math.max(bounds.top.min, moveState.coords.top) / moveState.maxY;
}
this.reposition(moveState.coords);
this.eventBroker("move", event);
}
pointerdown(event) {
document.body.setPointerCapture(event.pointerId);
event.preventDefault();
event.stopPropagation();
if (event.pointerId !== undefined) {
this.pointerId = event.pointerId;
}
if (!this.listening) {
document.body.addEventListener(
"pointerup",
(event) => {
if (this.isMoving) {
this.unbind(event);
}
},
false,
);
document.body.addEventListener(
"pointermove",
(event) => {
if (
this.pointerId !== undefined &&
event.pointerId === this.pointerId
) {
this.motionHandler(event);
}
},
false,
);
}
this.listening = true;
this.moveInit(event);
}
render() {
return html`<slot></slot>`;
}
}
if (!window.customElements.get("lit-movable")) {
window.customElements.define("lit-movable", LitMovable);
}

View File

@@ -1,27 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
export const loadHaServiceControl = async (): Promise<void> => {
if (customElements.get("ha-service-control")) {
return;
}
// Load in ha-service-control from developer-tools-service
const ppResolver = document.createElement("partial-panel-resolver");
const routes = (ppResolver as any).getRoutes([
{
component_name: "developer-tools",
url_path: "a",
},
]);
await routes?.routes?.a?.load?.();
const devToolsRouter = document.createElement("developer-tools-router");
const devToolsRoutes = (devToolsRouter as any)?.routerOptions?.routes;
if (devToolsRoutes?.service) {
await devToolsRoutes?.service?.load?.();
}
if (devToolsRoutes?.action) {
await devToolsRoutes?.action?.load?.();
}
};

View File

@@ -1,466 +0,0 @@
import {
Connection,
HassEntityAttributeBase,
HassServices,
MessageBase,
HassEntities as _HassEntities,
HassEntity as _HassEntity,
} from "home-assistant-js-websocket";
import { TemplateResult, nothing } from "lit";
import { ColorPicker as _ColorPicker } from "./lib/colorpicker/ColorPicker.js";
export type LitTemplateResult = typeof nothing | TemplateResult;
export type ColorPicker = _ColorPicker;
export interface Dictionary<TValue> {
[id: string]: TValue;
}
export interface ServiceCallRequest {
domain: string;
action: string;
service: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serviceData?: Record<string, any>;
target?: {
entity_id?: string | string[];
device_id?: string | string[];
area_id?: string | string[];
};
}
export interface HassEmptyEntity {
state: string;
attributes: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
};
}
export interface HassDevice {
id: string;
config_entries: string[];
name: string;
model?: string;
sw_version?: string;
primary_config_entry: string;
manufacturer: string | null;
serial_number: string | undefined;
connections: string[][];
}
export interface HassDeviceList {
[id: string]: HassDevice;
}
export type HassEntities = _HassEntities;
export type HassEntity = _HassEntity;
export type HassEntityInfo = {
entity_id: string;
device_id?: string;
labels?: string[];
translation_key?: string;
platform?: string;
name?: string;
};
export type HassEntityInfos = {
[entity_id: string]: HassEntityInfo;
};
export interface HomeAssistant {
connection: Connection;
language: string;
panels: {
[name: string]: {
component_name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config: { [key: string]: any } | null;
icon: string | null;
title: string | null;
url_path: string;
};
};
devices: HassDeviceList;
entities: HassEntityInfos;
states: HassEntities;
services: HassServices;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
localize: (key: string, ...args: any[]) => string;
translationMetadata: {
fragments: string[];
translations: {
[lang: string]: {
nativeName: string;
isRTL: boolean;
fingerprints: { [fragment: string]: string };
};
};
};
callApi: <T>(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters?: { [key: string]: any },
) => Promise<T>;
callService: (
domain: ServiceCallRequest["domain"],
action: ServiceCallRequest["action"],
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"],
) => Promise<void>;
callWS: <T>(msg: MessageBase) => Promise<T>;
}
export interface HassRoute {
prefix: string;
path: string;
}
export interface HaTextField {
name: string;
value: string;
}
export type HaFormData = string | number | boolean | string[];
export interface HaFormBaseSchema {
name: string;
default?: HaFormData;
required?: boolean;
description?: {
suffix?: string;
suggested_value?: HaFormData;
};
context?: Record<string, string>;
type?: never;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selector?: any;
}
export interface AnycubicFileLocal {
name: string;
size_mb: number;
}
export interface AnycubicFileCloud extends AnycubicFileLocal {
id: number;
}
export enum CalculatedTimeType {
ETA = "ETA",
Elapsed = "Elapsed",
Remaining = "Remaining",
}
export enum TemperatureUnit {
F = "F",
C = "C",
}
export enum StatTypeGeneral {
Status = "Status",
PrinterOnline = "Online",
Availability = "Availability",
ProjectName = "Project",
CurrentLayer = "Layer",
}
export enum StatTypeFDM {
HotendCurrent = "Hotend",
BedCurrent = "Bed",
HotendTarget = "T Hotend",
BedTarget = "T Bed",
DryingStatus = "Dry Status",
DryingTime = "Dry Time",
SpeedMode = "Speed Mode",
FanSpeed = "Fan Speed",
}
export enum StatTypeACE {
DryingStatus = "Dry Status",
DryingTime = "Dry Time",
}
export enum StatTypeLCD {
OnTime = "On Time",
OffTime = "Off Time",
BottomTime = "Bottom Time",
ModelHeight = "Model Height",
BottomLayers = "Bottom Layers",
ZUpHeight = "Z Up Height",
ZUpSpeed = "Z Up Speed",
ZDownSpeed = "Z Down Speed",
}
export const PrinterCardStatType = {
...CalculatedTimeType,
...StatTypeGeneral,
...StatTypeFDM,
...StatTypeACE,
...StatTypeLCD,
};
export type PrinterCardStatType =
| CalculatedTimeType
| StatTypeGeneral
| StatTypeFDM
| StatTypeACE
| StatTypeLCD;
export interface AnimatedPrinterBasicDimension {
width: number;
height: number;
}
export interface AnimatedPrinterXYDimension {
X: number;
Y: number;
}
export interface AnimatedPrinterLTDimension
extends AnimatedPrinterBasicDimension {
left: number;
top: number;
}
export interface AnimatedPrinterLTWidth {
width: number;
left: number;
top: number;
}
export interface AnimatedPrinterBuildPlateDimension {
maxWidth: number;
maxHeight: number;
verticalOffset: number;
}
export interface AnimatedPrinterAxisConfig
extends AnimatedPrinterBasicDimension {
stepper: boolean;
offsetLeft: number;
extruder: AnimatedPrinterBasicDimension;
}
export interface AnimatedPrinterConfig {
top: AnimatedPrinterBasicDimension;
bottom: AnimatedPrinterBasicDimension;
left: AnimatedPrinterBasicDimension;
right: AnimatedPrinterBasicDimension;
buildplate: AnimatedPrinterBuildPlateDimension;
xAxis: AnimatedPrinterAxisConfig;
}
export interface AnimatedPrinterDimensions {
Scalable: AnimatedPrinterBasicDimension;
Frame: AnimatedPrinterBasicDimension;
Hole: AnimatedPrinterLTDimension;
BuildArea: AnimatedPrinterLTDimension;
BuildPlate: AnimatedPrinterLTWidth;
XAxis: AnimatedPrinterLTDimension;
Track: AnimatedPrinterBasicDimension;
Basis: AnimatedPrinterXYDimension;
Gantry: AnimatedPrinterLTDimension;
Nozzle: AnimatedPrinterLTDimension;
GantryMaxLeft: number;
}
export interface AnycubicSpoolInfo {
material_type: string;
color: number[];
status: number;
spool_loaded: boolean;
}
export interface AnycubicSpeedMode {
description: string;
mode: number;
}
export interface SelectDropdownProps {
[key: string | number]: string;
}
export interface AnycubicSpeedModes {
[key: number]: string;
}
export interface AnycubicCardConfig {
printer_id?: string;
vertical?: boolean;
round?: boolean;
use_24hr?: boolean;
temperatureUnit?: TemperatureUnit;
lightEntityId?: string;
powerEntityId?: string;
cameraEntityId?: string;
monitoredStats?: PrinterCardStatType[];
scaleFactor?: number;
slotColors?: string[];
showSettingsButton?: boolean;
alwaysShow?: boolean;
}
export enum AnycubicMaterialType {
PLA = "PLA",
PETG = "PETG",
ABS = "ABS",
PACF = "PACF",
PC = "PC",
ASA = "ASA",
HIPS = "HIPS",
PA = "PA",
PLA_SE = "PLA_SE",
}
export enum AnycubicPrintOptionConfirmationType {
PAUSE = "pause",
RESUME = "resume",
CANCEL = "cancel",
}
export interface AnycubicFileListEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
file_info?: AnycubicFileLocal[];
};
}
export interface AnycubicCloudFileListEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
file_info?: AnycubicFileCloud[];
};
}
export interface AnycubicTargetTempEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
limit_min: number;
limit_max: number;
};
}
export interface AnycubicSpeedModeEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
available_modes: AnycubicSpeedMode[];
print_speed_mode_code: number;
};
}
export interface AnycubicDryingPresetEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
temperature?: number;
duration?: number;
};
}
export interface AnycubicSpoolInfoEntity extends HassEntity {
attributes: HassEntityAttributeBase & {
spool_info: AnycubicSpoolInfo[];
};
}
export interface TranslationDict {
[id: string]: string;
}
export interface HassPanel {
config: AnycubicCardConfig;
}
export interface PageChangeDetail {
item: Element;
}
export interface ModalEventBase {
modalOpen: boolean;
}
export interface ModalEventDrying extends ModalEventBase {
box_id: number | string;
}
export interface ModalEventSpool extends ModalEventBase {
box_id: number | string;
spool_index: number | string;
material_type?: string;
color: number[] | string | undefined;
}
export interface FormChangeDetail {
value: object;
}
export interface TextfieldChangeDetail<TValue> {
value: TValue;
}
export interface DropdownEvent<KValue, TValue> {
key: KValue;
value: TValue;
}
export interface ColourPickEvent {
color?: {
rgb: number[];
};
}
export interface HassServiceError {
message?: string;
}
export interface HassProgressButton {
actionSuccess: () => void;
actionError: () => void;
}
export interface CustomCardEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
documentationURL?: string;
}
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
}
export interface AnycubicLitNode {
route: HassRoute;
}
export interface DomClickEvent<T extends EventTarget> extends Event {
currentTarget: T;
}
export interface EvtTargPrinterDevId extends EventTarget {
printer_id: string;
}
export interface EvtTargConfirmationMode extends EventTarget {
confirmation_type: AnycubicPrintOptionConfirmationType;
}
export interface EvtTargFileInfo extends EventTarget {
file_info: AnycubicFileCloud | AnycubicFileLocal;
}
export interface EvtTargItemKey extends EventTarget {
item_key: string | number;
}
export interface EvtTargDirection extends EventTarget {
direction: number;
}
export interface EvtTargSpoolEdit extends EventTarget {
index: number;
material_type: string;
color: number[];
}
export interface EvtTargColourPreset extends EventTarget {
preset: string;
}

View File

@@ -1,91 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { getPrinterEntities } from "../../helpers";
import {
HassDevice,
HassDeviceList,
HassEntityInfos,
HassPanel,
HassRoute,
HomeAssistant,
LitTemplateResult,
} from "../../types";
@customElement("anycubic-view-debug")
export class AnycubicViewDebug extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property()
public route!: HassRoute;
@property()
public panel!: HassPanel;
@property()
public printers?: HassDeviceList;
@property({ attribute: "selected-printer-id" })
public selectedPrinterID: string | undefined;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@state()
private printerEntities: HassEntityInfos;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!changedProperties.has("selectedPrinterID")) {
return;
}
this.printerEntities = getPrinterEntities(
this.hass,
this.selectedPrinterID,
);
}
render(): LitTemplateResult {
return html`
<debug-data elevation="2">
<p>There are ${Object.keys(this.hass.states).length} entities.</p>
<p>The screen is${this.narrow ? "" : " not"} narrow.</p>
Configured panel config
<pre>${JSON.stringify(this.panel, undefined, 2)}</pre>
Current route
<pre>${JSON.stringify(this.route, undefined, 2)}</pre>
Printers
<pre>${JSON.stringify(this.printers, undefined, 2)}</pre>
Printer Entities
<pre>${JSON.stringify(this.printerEntities, undefined, 2)}</pre>
Selected Printer
<pre>${JSON.stringify(this.selectedPrinterDevice, undefined, 2)}</pre>
</debug-data>
`;
}
static get styles(): CSSResult {
return css`
:host {
padding: 16px;
display: block;
}
debug-data {
padding: 16px;
display: block;
font-size: 18px;
max-width: 600px;
margin: 0 auto;
}
`;
}
}

View File

@@ -1,91 +0,0 @@
import { CSSResult, css } from "lit";
export const commonFilesStyle: CSSResult = css`
:host {
padding: 16px;
display: block;
}
.files-card {
padding: 16px;
display: block;
font-size: 18px;
margin: 0 auto;
text-align: center;
}
.files-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
padding: 0;
margin: 0;
}
.file-info {
display: flex;
min-height: 20px;
min-width: 250px;
border: 2px solid #ccc3;
border-radius: 16px;
padding: 16px 32px;
line-height: 20px;
text-align: center;
font-weight: 900;
margin: 6px;
width: 100%;
justify-content: space-between;
}
.file-name {
display: block;
line-height: 20px;
text-align: center;
font-weight: 900;
margin: 6px;
word-wrap: break-word;
max-width: calc(100% - 58px);
}
.file-info:hover {
background-color: #ccc3;
border-color: #ccc9;
}
.file-refresh-button {
padding: 10px;
margin-bottom: 20px;
}
.file-refresh-icon {
--mdc-icon-size: 50px;
}
.file-delete-button {
padding: 4px;
margin-left: 10px;
}
.file-delete-icon {
}
.no-mqtt-msg {
}
@media (max-width: 599px) {
:host {
padding: 6px;
}
.files-card {
padding: 0px;
}
.file-info {
padding: 6px 6px;
margin: 6px 0px;
}
}
`;

View File

@@ -1,170 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { commonFilesStyle } from "./styles";
import { localize } from "../../../localize/localize";
import {
getPrinterEntities,
getPrinterEntityIdPart,
getPrinterSupportsMQTT,
} from "../../helpers";
import {
AnycubicFileLocal,
DomClickEvent,
EvtTargFileInfo,
HassDevice,
HassEntityInfo,
HassEntityInfos,
HassPanel,
HassRoute,
HomeAssistant,
LitTemplateResult,
} from "../../types";
export class AnycubicViewFilesBase extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property()
public route!: HassRoute;
@property()
public panel!: HassPanel;
@property({ attribute: "selected-printer-id" })
public selectedPrinterID: string | undefined;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@state()
protected printerEntities: HassEntityInfos;
@state()
private printerEntityIdPart: string | undefined;
@state()
protected _fileArray: AnycubicFileLocal[] | undefined;
@state()
protected _listRefreshEntity: HassEntityInfo | undefined;
@state()
private _isRefreshing: boolean = false;
@state()
protected _isDeleting: boolean;
@state()
private _noMqttMessage: string;
@state()
private _supportsMQTT: boolean = false;
@state()
protected _httpResponse: boolean = false;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._noMqttMessage = localize(
"common.messages.mqtt_unsupported",
this.language,
);
}
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterID")
) {
this.printerEntities = getPrinterEntities(
this.hass,
this.selectedPrinterID,
);
this.printerEntityIdPart = getPrinterEntityIdPart(this.printerEntities);
this._supportsMQTT = getPrinterSupportsMQTT(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
);
}
}
render(): LitTemplateResult {
return html`
<div class="files-card" elevation="2">
<button
.disabled=${(!this._httpResponse && !this._supportsMQTT) || this._isRefreshing}
class="file-refresh-button"
@click=${this.refreshList}
>
<ha-icon
class="file-refresh-icon"
icon="mdi:refresh"
>
</ha-icon>
</button>
${
!this._httpResponse && !this._supportsMQTT
? html` <div class="no-mqtt-msg">${this._noMqttMessage}</div> `
: nothing
}
<ul class="files-container">
${
this._fileArray
? this._fileArray.map(
(fileInfo) => html`
<li class="file-info">
<div class="file-name">${fileInfo.name}</div>
<button
class="file-delete-button"
.disabled=${this._isDeleting}
.file_info=${fileInfo}
@click=${this.deleteFile}
>
<ha-icon
class="file-delete-icon"
icon="mdi:delete"
></ha-icon>
</button>
</li>
`,
)
: null
}
</div>
`;
}
refreshList = (): void => {
if (this._listRefreshEntity) {
this._isRefreshing = true;
this.hass
.callService("button", "press", {
entity_id: this._listRefreshEntity.entity_id,
})
.then(() => {
this._isRefreshing = false;
})
.catch((_e: unknown) => {
this._isRefreshing = false;
});
}
};
// eslint-disable-next-line no-empty-function
deleteFile = (_ev: DomClickEvent<EvtTargFileInfo>): void => {};
static get styles(): CSSResult {
return css`
${commonFilesStyle}
`;
}
}

View File

@@ -1,66 +0,0 @@
import { PropertyValues } from "lit";
import { customElement, state } from "lit/decorators.js";
import { AnycubicViewFilesBase } from "./view-files_base";
import { platform } from "../../const";
import {
getEntityState,
getFileListCloudFilesEntity,
getFileListCloudRefreshEntity,
} from "../../helpers";
import {
AnycubicCloudFileListEntity,
AnycubicFileCloud,
DomClickEvent,
EvtTargFileInfo,
} from "../../types";
@customElement("anycubic-view-files_cloud")
export class AnycubicViewFilesCloud extends AnycubicViewFilesBase {
@state()
protected _fileArray: AnycubicFileCloud[] | undefined;
@state()
protected _httpResponse: boolean = true;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterID")
) {
const fileListState: AnycubicCloudFileListEntity | undefined =
getEntityState(
this.hass,
getFileListCloudFilesEntity(this.printerEntities),
);
this._fileArray = fileListState
? fileListState.attributes.file_info
: undefined;
this._listRefreshEntity = getFileListCloudRefreshEntity(
this.printerEntities,
);
}
}
deleteFile = (ev: DomClickEvent<EvtTargFileInfo>): void => {
const fileInfo: AnycubicFileCloud = ev.currentTarget
.file_info as AnycubicFileCloud;
if (this.selectedPrinterDevice && fileInfo.id) {
this._isDeleting = true;
this.hass
.callService(platform, "delete_file_cloud", {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
file_id: fileInfo.id,
})
.then(() => {
this._isDeleting = false;
})
.catch((_e: unknown) => {
this._isDeleting = false;
});
}
};
}

View File

@@ -1,59 +0,0 @@
import { PropertyValues } from "lit";
import { customElement } from "lit/decorators.js";
import { AnycubicViewFilesBase } from "./view-files_base";
import { platform } from "../../const";
import {
getEntityState,
getFileListLocalFilesEntity,
getFileListLocalRefreshEntity,
} from "../../helpers";
import {
AnycubicFileListEntity,
AnycubicFileLocal,
DomClickEvent,
EvtTargFileInfo,
} from "../../types";
@customElement("anycubic-view-files_local")
export class AnycubicViewFilesLocal extends AnycubicViewFilesBase {
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterID")
) {
const fileListState: AnycubicFileListEntity | undefined = getEntityState(
this.hass,
getFileListLocalFilesEntity(this.printerEntities),
);
this._fileArray = fileListState
? fileListState.attributes.file_info
: undefined;
this._listRefreshEntity = getFileListLocalRefreshEntity(
this.printerEntities,
);
}
}
deleteFile = (ev: DomClickEvent<EvtTargFileInfo>): void => {
const fileInfo: AnycubicFileLocal = ev.currentTarget
.file_info as AnycubicFileLocal;
if (this.selectedPrinterDevice && fileInfo.name) {
this._isDeleting = true;
this.hass
.callService(platform, "delete_file_local", {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
filename: fileInfo.name,
})
.then(() => {
this._isDeleting = false;
})
.catch((_e: unknown) => {
this._isDeleting = false;
});
}
};
}

View File

@@ -1,59 +0,0 @@
import { PropertyValues } from "lit";
import { customElement } from "lit/decorators.js";
import { AnycubicViewFilesBase } from "./view-files_base";
import { platform } from "../../const";
import {
getEntityState,
getFileListUdiskFilesEntity,
getFileListUdiskRefreshEntity,
} from "../../helpers";
import {
AnycubicFileListEntity,
AnycubicFileLocal,
DomClickEvent,
EvtTargFileInfo,
} from "../../types";
@customElement("anycubic-view-files_udisk")
export class AnycubicViewFilesUdisk extends AnycubicViewFilesBase {
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterID")
) {
const fileListState: AnycubicFileListEntity | undefined = getEntityState(
this.hass,
getFileListUdiskFilesEntity(this.printerEntities),
);
this._fileArray = fileListState
? fileListState.attributes.file_info
: undefined;
this._listRefreshEntity = getFileListUdiskRefreshEntity(
this.printerEntities,
);
}
}
deleteFile = (ev: DomClickEvent<EvtTargFileInfo>): void => {
const fileInfo: AnycubicFileLocal = ev.currentTarget
.file_info as AnycubicFileLocal;
if (this.selectedPrinterDevice && fileInfo.name) {
this._isDeleting = true;
this.hass
.callService(platform, "delete_file_udisk", {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
filename: fileInfo.name,
})
.then(() => {
this._isDeleting = false;
})
.catch((_e: unknown) => {
this._isDeleting = false;
});
}
};
}

View File

@@ -1,443 +0,0 @@
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { localize } from "../../../localize/localize";
import {
getPanelACEMonitoredStats,
getPanelBasicMonitoredStats,
getPanelFDMMonitoredStats,
getPrinterBinarySensorState,
getPrinterEntities,
getPrinterEntityIdPart,
getPrinterID,
getPrinterMAC,
getPrinterSensorStateFloat,
getPrinterSensorStateString,
getPrinterUpdateEntityState,
isFDMPrinter,
} from "../../helpers";
import {
HassDevice,
HassEntityInfos,
HassPanel,
HassRoute,
HomeAssistant,
LitTemplateResult,
PrinterCardStatType,
TranslationDict,
} from "../../types";
import "../../components/printer_card/card/card.ts";
const monitoredStatsACE: PrinterCardStatType[] = getPanelACEMonitoredStats();
const monitoredStatsBasic: PrinterCardStatType[] =
getPanelBasicMonitoredStats();
const monitoredStatsFDM: PrinterCardStatType[] = getPanelFDMMonitoredStats();
const infoFields: string[] = [
"printer_name",
"printer_id",
"printer_mac",
"printer_model",
"printer_fw_version",
"printer_fw_update_available",
"printer_online",
"printer_available",
"curr_nozzle_temp",
"curr_hotbed_temp",
"target_nozzle_temp",
"target_hotbed_temp",
"job_state",
"job_progress",
"ace_fw_version",
"ace_fw_update_available",
"drying_active",
"drying_progress",
];
@customElement("anycubic-view-main")
export class AnycubicViewMain extends LitElement {
@property()
public hass!: HomeAssistant;
@property()
public language!: string;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property()
public route!: HassRoute;
@property()
public panel!: HassPanel;
@property({ attribute: "selected-printer-id" })
public selectedPrinterID: string | undefined;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@state()
private printerEntities: HassEntityInfos;
@state()
private printerEntityIdPart: string | undefined;
@state()
private printerID: string | undefined;
@state()
private printerMAC: string | null;
@state()
private printerStateFwUpdateAvailable: string | undefined;
@state()
private printerStateAvailable: string | boolean | undefined;
@state()
private printerStateOnline: string | boolean | undefined;
@state()
private printerStateCurrNozzleTemp: number | undefined;
@state()
private printerStateCurrHotbedTemp: number | undefined;
@state()
private printerStateTargetNozzleTemp: number | undefined;
@state()
private printerStateTargetHotbedTemp: number | undefined;
@state()
private jobStateProgress: string | undefined;
@state()
private jobStatePrintState: string | undefined;
@state()
private aceStateFwUpdateAvailable: string | boolean | undefined;
@state()
private aceStateDryingActive: string | boolean | undefined;
@state()
private aceStateDryingRemaining: number | undefined;
@state()
private aceStateDryingTotal: number | undefined;
@state()
private aceDryingProgress: string | undefined;
@state()
private isFDM: boolean = false;
@state()
private monitoredStats: PrinterCardStatType[] = monitoredStatsBasic;
@state()
private _statTranslations: TranslationDict;
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._statTranslations = infoFields.reduce((fConf, fieldKey) => {
fConf[fieldKey] = localize(
`panels.main.cards.main.fields.${fieldKey}`,
this.language,
);
return fConf;
}, {});
}
if (changedProperties.has("selectedPrinterDevice")) {
this.printerID = getPrinterID(this.selectedPrinterDevice);
this.printerMAC = getPrinterMAC(this.selectedPrinterDevice);
}
if (changedProperties.has("selectedPrinterID")) {
this.printerEntities = getPrinterEntities(
this.hass,
this.selectedPrinterID,
);
this.printerEntityIdPart = getPrinterEntityIdPart(this.printerEntities);
}
if (
changedProperties.has("hass") ||
changedProperties.has("selectedPrinterID")
) {
this.isFDM = isFDMPrinter(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
);
this.printerStateFwUpdateAvailable = getPrinterUpdateEntityState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"printer_firmware",
);
this.printerStateAvailable = getPrinterBinarySensorState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"is_available",
"Available",
"Busy",
);
this.printerStateOnline = getPrinterBinarySensorState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"printer_online",
"Online",
"Offline",
);
this.printerStateCurrNozzleTemp = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"nozzle_temperature",
);
this.printerStateCurrHotbedTemp = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"hotbed_temperature",
);
this.printerStateTargetNozzleTemp = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_nozzle_temperature",
);
this.printerStateTargetHotbedTemp = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"target_hotbed_temperature",
);
const projProgress = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_progress",
);
this.jobStateProgress =
typeof projProgress !== "undefined" ? `${projProgress}%` : "0%";
this.jobStatePrintState = getPrinterSensorStateString(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"job_state",
true,
);
this.aceStateFwUpdateAvailable = getPrinterUpdateEntityState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"ace_firmware",
);
this.aceStateDryingActive = getPrinterBinarySensorState(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_active",
"Drying",
"Not Drying",
);
this.aceStateDryingRemaining = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_remaining_time",
);
this.aceStateDryingTotal = getPrinterSensorStateFloat(
this.hass,
this.printerEntities,
this.printerEntityIdPart,
"drying_total_duration",
);
this.aceDryingProgress =
typeof this.aceStateDryingRemaining !== "undefined" &&
typeof this.aceStateDryingTotal !== "undefined"
? String(
(this.aceStateDryingTotal > 0
? Math.round(
(1 -
this.aceStateDryingRemaining / this.aceStateDryingTotal) *
10000,
) / 100
: 0
).toFixed(2),
) + "%"
: undefined;
if (this.aceStateFwUpdateAvailable) {
this.monitoredStats = monitoredStatsACE;
} else if (this.isFDM) {
this.monitoredStats = monitoredStatsFDM;
} else {
this.monitoredStats = monitoredStatsBasic;
}
}
}
private _renderInfoRow(
fieldKey: string,
rowData: string | number | boolean | undefined | null,
): LitTemplateResult {
return html`
<div class="info-row">
<span class="info-heading"> ${this._statTranslations[fieldKey]}:</span>
<span class="info-detail">${rowData}</span>
</div>
`;
}
private _renderOptionalInfoRow(
fieldKey: string,
rowData: string | number | boolean | undefined | null,
): LitTemplateResult | null {
return typeof rowData !== "undefined"
? this._renderInfoRow(fieldKey, rowData)
: null;
}
render(): LitTemplateResult {
return html`
<printer-card elevation="2">
<anycubic-printercard-card
.hass=${this.hass}
.language=${this.language}
.selectedPrinterID=${this.selectedPrinterID}
.selectedPrinterDevice=${this.selectedPrinterDevice}
.vertical=${this.panel.config.vertical ?? false}
.round=${this.panel.config.round ?? false}
.use_24hr=${this.panel.config.use_24hr ?? true}
.temperatureUnit=${this.panel.config.temperatureUnit}
.lightEntityId=${this.panel.config.lightEntityId}
.powerEntityId=${this.panel.config.powerEntityId}
.cameraEntityId=${this.panel.config.cameraEntityId}
.monitoredStats=${this.panel.config.monitoredStats ??
this.monitoredStats}
.scaleFactor=${this.panel.config.scaleFactor}
.slotColors=${this.panel.config.slotColors}
.showSettingsButton=${this.panel.config.showSettingsButton ?? true}
.alwaysShow=${this.panel.config.alwaysShow}
></anycubic-printercard-card>
<div class="ac-extra-printer-info">
${this._renderInfoRow(
"printer_name",
this.selectedPrinterDevice ? this.selectedPrinterDevice.name : null,
)}
${this._renderInfoRow("printer_id", this.printerID)}
${this._renderInfoRow("printer_mac", this.printerMAC)}
${this._renderInfoRow(
"printer_model",
this.selectedPrinterDevice
? this.selectedPrinterDevice.model
: null,
)}
${this._renderInfoRow(
"printer_fw_version",
this.selectedPrinterDevice
? this.selectedPrinterDevice.sw_version
: null,
)}
${this._renderInfoRow(
"printer_fw_update_available",
this.printerStateFwUpdateAvailable,
)}
${this._renderInfoRow("printer_online", this.printerStateOnline)}
${this._renderInfoRow(
"printer_available",
this.printerStateAvailable,
)}
${this.isFDM
? html`
${this._renderInfoRow(
"curr_nozzle_temp",
this.printerStateCurrNozzleTemp,
)}
${this._renderInfoRow(
"curr_hotbed_temp",
this.printerStateCurrHotbedTemp,
)}
${this._renderInfoRow(
"target_nozzle_temp",
this.printerStateTargetNozzleTemp,
)}
${this._renderInfoRow(
"target_hotbed_temp",
this.printerStateTargetHotbedTemp,
)}
`
: nothing}
${this._renderInfoRow("job_state", this.jobStatePrintState)}
${this._renderInfoRow("job_progress", this.jobStateProgress)}
${this._renderOptionalInfoRow(
"ace_fw_update_available",
this.aceStateFwUpdateAvailable,
)}
${this._renderOptionalInfoRow(
"drying_active",
this.aceStateDryingActive,
)}
${this._renderOptionalInfoRow(
"drying_progress",
this.aceDryingProgress,
)}
</div>
</printer-card>
`;
}
static get styles(): CSSResult {
return css`
:host {
padding: 16px;
display: block;
}
printer-card {
padding: 16px;
display: block;
font-size: 18px;
max-width: 600px;
margin: 0 auto;
}
anycubic-printercard-card {
margin: 24px;
}
.ac-extra-printer-info {
padding: 20px 40px;
}
.info-row {
margin-bottom: 6px;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
box-sizing: border-box;
width: 100%;
}
.info-heading {
margin-right: 10px;
font-size: 0.85em;
}
.info-detail {
font-weight: 700;
}
`;
}
}

View File

@@ -1,28 +0,0 @@
import { CSSResult, css } from "lit";
export const commonPrintStyle: CSSResult = css`
:host {
padding: 16px;
display: block;
}
ac-print-view {
padding: 16px;
display: block;
font-size: 18px;
max-width: 1024px;
margin: 0 auto;
}
ha-alert {
margin-top: 10px;
margin-bottom: 10px;
}
.print-button {
margin: auto;
width: 100px;
height: 40px;
display: block;
margin-top: 20px;
}
`;

View File

@@ -1,146 +0,0 @@
import { mdiPlay } from "@mdi/js";
import { CSSResult, LitElement, PropertyValues, css, html, nothing } from "lit";
import { property, state } from "lit/decorators.js";
import { commonPrintStyle } from "./styles";
import { localize } from "../../../localize/localize";
import { platform } from "../../const";
import { HASSDomEvent } from "../../fire_event";
import { fireHaptic } from "../../fire_haptic";
import { loadHaServiceControl } from "../../load-ha-elements";
import {
FormChangeDetail,
HassDevice,
HassPanel,
HassProgressButton,
HassRoute,
HassServiceError,
HomeAssistant,
LitTemplateResult,
} from "../../types";
export class AnycubicViewPrintBase extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property()
public language!: string;
@property({ type: Boolean, reflect: true })
public narrow!: boolean;
@property()
public route!: HassRoute;
@property()
public panel!: HassPanel;
@property({ attribute: "selected-printer-id" })
public selectedPrinterID: string | undefined;
@property({ attribute: "selected-printer-device" })
public selectedPrinterDevice: HassDevice | undefined;
@state() private _scriptData: Record<
string,
string | Record<string, string> | undefined
> = {};
@state()
private _error: string | undefined;
@state()
protected _serviceName: string = "";
@state()
private _buttonPrint: string;
@state()
private _buttonProgress: boolean = false;
async firstUpdated(): Promise<void> {
await loadHaServiceControl();
}
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("language")) {
this._buttonPrint = localize("common.actions.print", this.language);
}
if (changedProperties.has("selectedPrinterDevice")) {
if (this.selectedPrinterDevice) {
const srvName = `${platform}.${this._serviceName}`;
this._scriptData = {
...this._scriptData,
action: srvName,
service: srvName,
data: {
...((this._scriptData.data as object | undefined) ||
({} as object)),
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
},
};
}
}
}
render(): LitTemplateResult {
return html`
<ac-print-view elevation="2">
<ha-service-control
hidePicker
.hass=${this.hass}
.value=${this._scriptData}
.showAdvanced=${true}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
${this._error !== undefined
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-progress-button
class="print-button"
raised
@click=${this._runScript}
.progress=${this._buttonProgress}
>
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
${this._buttonPrint}
</ha-progress-button>
</ac-print-view>
`;
}
private _scriptDataChanged = (ev: HASSDomEvent<FormChangeDetail>): void => {
this._scriptData = { ...this._scriptData, ...ev.detail.value };
this._error = undefined;
};
private _runScript = (ev: Event): void => {
const button = ev.currentTarget as unknown as HassProgressButton;
this._error = undefined;
ev.stopPropagation();
this._buttonProgress = true;
fireHaptic();
this.hass
.callService(platform, this._serviceName, this._scriptData.data as object)
.then(() => {
button.actionSuccess();
this._buttonProgress = false;
})
.catch((e: unknown) => {
this._error = (e as HassServiceError).message;
button.actionError();
this._buttonProgress = false;
});
};
static get styles(): CSSResult {
return css`
${commonPrintStyle}
`;
}
}

View File

@@ -1,9 +0,0 @@
import { customElement, state } from "lit/decorators.js";
import { AnycubicViewPrintBase } from "./view-print-base";
@customElement("anycubic-view-print-no_cloud_save")
export class AnycubicViewPrintNoCloudSave extends AnycubicViewPrintBase {
@state()
protected _serviceName: string = "print_and_upload_no_cloud_save";
}

View File

@@ -1,9 +0,0 @@
import { customElement, state } from "lit/decorators.js";
import { AnycubicViewPrintBase } from "./view-print-base";
@customElement("anycubic-view-print-save_in_cloud")
export class AnycubicViewPrintSaveInCloud extends AnycubicViewPrintBase {
@state()
protected _serviceName: string = "print_and_upload_save_in_cloud";
}

View File

@@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"es2017",
"dom",
"dom.iterable"
],
"noEmit": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strict": true,
"noImplicitAny": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"experimentalDecorators": true,
"useDefineForClassFields": false,
"strictPropertyInitialization": false,
"allowSyntheticDefaultImports": true
}
}

View File

@@ -5,8 +5,9 @@
"@Gangoke"
],
"config_flow": true,
"homeassistant": "2026.3.0",
"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"
"version": "0.3.0"
}

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
from typing import Any
from homeassistant.components.number import NumberEntity
from .const import DOMAIN
from .entity import KobraXEntity
def _minutes_to_hhmmss(minutes: int | float) -> str:
"""Convert minutes to HH:mm:ss format."""
total_seconds = int(minutes * 60)
hours = total_seconds // 3600
remaining_seconds = total_seconds % 3600
mins = remaining_seconds // 60
secs = remaining_seconds % 60
return f"{hours:02d}:{mins:02d}:{secs:02d}"
class KobraXAceDryConfigNumber(KobraXEntity, NumberEntity):
def __init__(
self,
coordinator,
entry,
ace_id: int,
config_type: str, # "target_temp" or "duration"
) -> None:
if config_type == "target_temp":
unique_key = f"ace_{ace_id}_dry_target_temp"
name = f"ACE {ace_id + 1} Dryer Target Temperature"
min_val, max_val, step_val = 30, 80, 1
unit = "°C"
icon = "mdi:thermometer"
else: # duration
unique_key = f"ace_{ace_id}_dry_duration"
name = f"ACE {ace_id + 1} Dryer Duration"
min_val, max_val, step_val = 10, 1440, 1
unit = "min"
icon = "mdi:timer-cog-outline"
super().__init__(coordinator, entry, unique_key, name)
self._ace_id = ace_id
self._config_type = config_type
self._attr_native_min_value = min_val
self._attr_native_max_value = max_val
self._attr_native_step = step_val
self._attr_native_unit_of_measurement = unit
self._attr_mode = "box"
self._attr_icon = icon
@property
def native_value(self) -> float:
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]
ace_cfg = cfg.get(self._ace_id) or {}
key = "target_temp" if self._config_type == "target_temp" else "duration"
if self._config_type == "target_temp":
return float(ace_cfg.get(key, 45))
else:
return float(ace_cfg.get(key, 240))
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
if self._config_type != "duration":
return None
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]
ace_cfg = cfg.get(self._ace_id) or {}
duration_minutes = ace_cfg.get("duration", 240)
return {"formatted_duration": _minutes_to_hhmmss(duration_minutes)}
async def async_set_native_value(self, value: float) -> None:
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]
if self._ace_id not in cfg:
cfg[self._ace_id] = {}
key = "target_temp" if self._config_type == "target_temp" else "duration"
cfg[self._ace_id][key] = int(round(value))
self.async_write_ha_state()
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
# Initialize ace_dry_config structure if not present
if "ace_dry_config" not in hass.data[DOMAIN][entry.entry_id]:
hass.data[DOMAIN][entry.entry_id]["ace_dry_config"] = {}
# Pre-create all 8 numbers (target_temp + duration for each of 4 ACE units)
entities = []
for ace_id in range(4):
entities.append(KobraXAceDryConfigNumber(coordinator, entry, ace_id, "target_temp"))
entities.append(KobraXAceDryConfigNumber(coordinator, entry, ace_id, "duration"))
async_add_entities(entities)

View File

@@ -1,13 +1,34 @@
from __future__ import annotations
from typing import Any
from homeassistant.components.select import SelectEntity
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity import EntityCategory
from .api import KobraXApiError
from .const import DOMAIN
from .entity import KobraXEntity
MAX_FILAMENT_SLOTS = 19
TOOLHEAD_SLOT_LIMIT = 4
ACE_DIRECT_SLOT_LIMIT = 4
def _detected_slot_limit(state_data: dict[str, Any]) -> int:
mode = str(state_data.get("filament_mode") or "toolhead").lower()
if mode == "toolhead":
return TOOLHEAD_SLOT_LIMIT
if mode == "ace_direct":
return ACE_DIRECT_SLOT_LIMIT
slots = state_data.get("ams_slots") or []
if isinstance(slots, list) and slots:
return min(len(slots), MAX_FILAMENT_SLOTS)
return MAX_FILAMENT_SLOTS
class KobraXPrintSpeedSelect(KobraXEntity, SelectEntity):
_attr_options = ["Slow (1)", "Normal (2)", "Fast (3)"]
@@ -34,8 +55,139 @@ class KobraXPrintSpeedSelect(KobraXEntity, SelectEntity):
raise ServiceValidationError(str(err)) from err
class KobraXFilamentProfileSelect(KobraXEntity, SelectEntity):
_AUTO_OPTION = "Auto (No Override)"
def __init__(self, coordinator, entry, slot_index: int) -> None:
super().__init__(coordinator, entry, f"slot_{slot_index + 1}_filament_profile", f"Slot {slot_index + 1} Filament Profile")
self._slot_index = slot_index
self._attr_icon = "mdi:palette"
self._attr_entity_category = EntityCategory.CONFIG
def _slot_limit_for_mode(self) -> int:
return _detected_slot_limit(self.state_data)
def _slot(self) -> dict[str, Any]:
slots = self.state_data.get("filament_slots") or []
if not isinstance(slots, list):
return {}
for slot in slots:
if not isinstance(slot, dict):
continue
try:
if int(slot.get("slot_index", -1)) == self._slot_index:
return slot
except (TypeError, ValueError):
continue
return {}
def _profile_catalog(self) -> list[dict[str, Any]]:
data = self.hass.data[DOMAIN][self._entry.entry_id]
profiles = data.get("filament_profiles")
if isinstance(profiles, list):
return [item for item in profiles if isinstance(item, dict)]
return []
@staticmethod
def _profile_label(profile: dict[str, Any]) -> str:
name = str(profile.get("name") or profile.get("id") or "Unknown")
vendor = str(profile.get("vendor") or "")
fid = str(profile.get("id") or "")
vendor_part = f" - {vendor}" if vendor else ""
return f"{name}{vendor_part} [{fid}]" if fid else f"{name}{vendor_part}"
def _options_map(self) -> dict[str, tuple[str, str, str]]:
slot = self._slot()
material = str(slot.get("material") or "").upper()
options: dict[str, tuple[str, str, str]] = {self._AUTO_OPTION: ("", "", "")}
for profile in self._profile_catalog():
profile_type = str(profile.get("type") or "").upper()
if material and profile_type and profile_type != material:
continue
fid = str(profile.get("id") or "")
if not fid:
continue
vendor = str(profile.get("vendor") or "")
name = str(profile.get("name") or "")
options[self._profile_label(profile)] = (fid, vendor, name)
return options
@property
def available(self) -> bool:
return self._slot_index < self._slot_limit_for_mode() and bool(self._slot()) and super().available
@property
def options(self) -> list[str]:
return list(self._options_map().keys())
@property
def current_option(self) -> str:
slot = self._slot()
filament_id = str(slot.get("filament_id") or "")
vendor = str(slot.get("filament_vendor") or "")
name = str(slot.get("filament_name") or "")
if not filament_id:
return self._AUTO_OPTION
option_map = self._options_map()
for label, (opt_id, opt_vendor, opt_name) in option_map.items():
if name and vendor and opt_name == name and opt_vendor == vendor:
return label
if opt_id == filament_id and opt_vendor == vendor:
return label
if opt_id == filament_id and not vendor:
return label
return f"Custom [{filament_id}]"
def _apply_optimistic_state(self, filament_id: str, vendor: str, name: str) -> None:
merged: dict[str, Any] = dict(self.coordinator.data or {})
slots = merged.get("filament_slots")
if not isinstance(slots, list):
return
new_slots: list[dict[str, Any]] = []
for slot in slots:
if not isinstance(slot, dict):
continue
next_slot = dict(slot)
try:
if int(next_slot.get("slot_index", -1)) == self._slot_index:
next_slot["filament_id"] = filament_id
next_slot["filament_vendor"] = vendor
next_slot["filament_name"] = name
except (TypeError, ValueError):
pass
new_slots.append(next_slot)
merged["filament_slots"] = new_slots
self.coordinator.async_set_updated_data(merged)
async def async_select_option(self, option: str) -> None:
option_map = self._options_map()
if option not in option_map:
raise ServiceValidationError(f"Invalid option: {option}")
filament_id, vendor, name = option_map[option]
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
await api.async_set_filament_slot_profile(self._slot_index, filament_id, vendor, name)
self._apply_optimistic_state(filament_id, vendor, name)
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
api = hass.data[DOMAIN][entry.entry_id]["api"]
try:
hass.data[DOMAIN][entry.entry_id]["filament_profiles"] = await api.async_get_filament_profiles()
except KobraXApiError:
hass.data[DOMAIN][entry.entry_id]["filament_profiles"] = []
entities: list[SelectEntity] = [KobraXPrintSpeedSelect(coordinator, entry, "print_speed_mode", "Print Speed")]
entities.extend(KobraXFilamentProfileSelect(coordinator, entry, slot_index) for slot_index in range(MAX_FILAMENT_SLOTS))
async_add_entities(
[KobraXPrintSpeedSelect(coordinator, entry, "print_speed_mode", "Print Speed")]
entities
)

View File

@@ -2,14 +2,45 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from urllib.parse import quote
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorEntityDescription
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
from .const import DOMAIN
from .entity import KobraXEntity
MAX_FILAMENT_SLOTS = 19
TOOLHEAD_SLOT_LIMIT = 4
ACE_DIRECT_SLOT_LIMIT = 4
MAX_ACE_UNITS = 4
def _detected_slot_limit(state_data: dict[str, Any]) -> int:
mode = str(state_data.get("filament_mode") or "toolhead").lower()
if mode == "toolhead":
return TOOLHEAD_SLOT_LIMIT
if mode == "ace_direct":
return ACE_DIRECT_SLOT_LIMIT
slots = state_data.get("ams_slots") or []
if isinstance(slots, list) and slots:
return min(len(slots), MAX_FILAMENT_SLOTS)
return MAX_FILAMENT_SLOTS
def _detected_ace_unit_count(state_data: dict[str, Any]) -> int:
units = state_data.get("ace_units") or []
if isinstance(units, list):
return min(len(units), MAX_ACE_UNITS)
return 0
@dataclass(frozen=True, kw_only=True)
class KobraXSensorDescription(SensorEntityDescription):
value_key: str
@@ -97,105 +128,41 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="print_duration",
icon="mdi:timer-outline",
),
# Compatibility aliases used by the kobrax-lan-card (ported from anycubic card)
KobraXSensorDescription(
key="job_state",
name="Job State",
value_key="print_state",
icon="mdi:state-machine",
key="skip_object_count",
name="Skip Object Count",
value_key="skip_object_count",
icon="mdi:vector-polygon",
),
KobraXSensorDescription(
key="current_status",
name="Current Status",
value_key="print_state",
icon="mdi:information-outline",
key="skipped_object_count",
name="Skipped Object Count",
value_key="skipped_object_count",
icon="mdi:content-cut",
),
KobraXSensorDescription(
key="job_progress",
name="Job Progress",
value_key="progress",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
key="filament_mode",
name="Filament Mode",
value_key="filament_mode",
icon="mdi:shape-outline",
),
KobraXSensorDescription(
key="job_time_elapsed",
name="Job Time Elapsed",
value_key="print_duration",
icon="mdi:timer-outline",
key="ace_unit_count",
name="ACE Unit Count",
value_key="ace_unit_count",
icon="mdi:package-variant",
),
KobraXSensorDescription(
key="job_time_remaining",
name="Job Time Remaining",
value_key="remain_time",
icon="mdi:timer-sand",
key="bridge_version",
name="Bridge Version",
value_key="version",
icon="mdi:source-branch",
),
KobraXSensorDescription(
key="hotbed_temperature",
name="Hotbed Temperature",
value_key="bed_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="nozzle_temperature",
name="Nozzle Temperature",
value_key="nozzle_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_hotbed_temperature",
name="Target Hotbed Temperature",
value_key="bed_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_nozzle_temperature",
name="Target Nozzle Temperature",
value_key="nozzle_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="job_name",
name="Job Name",
value_key="filename",
icon="mdi:file",
),
KobraXSensorDescription(
key="job_current_layer",
name="Job Current Layer",
value_key="curr_layer",
icon="mdi:layers-triple",
),
KobraXSensorDescription(
key="job_total_layers",
name="Job Total Layers",
value_key="total_layers",
icon="mdi:layers",
),
KobraXSensorDescription(
key="job_speed_mode",
name="Job Speed Mode",
value_key="print_speed_mode",
icon="mdi:speedometer",
),
KobraXSensorDescription(
key="fan_speed",
name="Fan Speed",
value_key="fan_speed",
icon="mdi:fan",
),
KobraXSensorDescription(
key="ace_spools",
name="ACE Spools",
value_key="ams_slots",
icon="mdi:palette-swatch",
key="latest_available_version",
name="Latest Available Version",
value_key="latest_available_version",
icon="mdi:cloud-download-outline",
),
)
@@ -206,66 +173,183 @@ class KobraXSensor(KobraXEntity, SensorEntity):
def __init__(self, coordinator, entry, description: KobraXSensorDescription) -> None:
super().__init__(coordinator, entry, description.key, description.name)
self.entity_description = description
if description.value_key in ("version", "latest_available_version"):
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@staticmethod
def _seconds_to_hhmmss(seconds: int) -> str:
hours = seconds // 3600
remaining_seconds = seconds % 3600
minutes = remaining_seconds // 60
secs = remaining_seconds % 60
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
@property
def native_value(self) -> Any:
if self.entity_description.value_key == "filament_mode":
return self.state_data.get("filament_mode")
if self.entity_description.value_key == "ace_unit_count":
units = self.state_data.get("ace_units")
return len(units) if isinstance(units, list) else 0
if self.entity_description.value_key == "latest_available_version":
update_info = self.state_data.get("update_info") or {}
if isinstance(update_info, dict):
return update_info.get("latest")
return None
if self.entity_description.value_key == "skip_object_count":
skip_state = self.state_data.get("skip_state") or {}
objects = skip_state.get("objects") if isinstance(skip_state, dict) else []
return len(objects) if isinstance(objects, list) else 0
if self.entity_description.value_key == "skipped_object_count":
skip_state = self.state_data.get("skip_state") or {}
skipped = skip_state.get("skipped") if isinstance(skip_state, dict) else []
return len(skipped) if isinstance(skipped, list) else 0
value = self.state_data.get(self.entity_description.value_key)
if self.entity_description.key in {"progress", "job_progress"} and value is not None:
if self.entity_description.value_key == "progress" and value is not None:
return round(float(value) * 100, 1)
if self.entity_description.key == "job_speed_mode":
mode = int(value) if value is not None else 2
mapping = {1: "Slow", 2: "Normal", 3: "Fast"}
return mapping.get(mode, "Normal")
if self.entity_description.key == "ace_spools":
return "active" if isinstance(value, list) and len(value) > 0 else "inactive"
if self.entity_description.value_key in ("remain_time", "print_duration") and value is not None:
try:
return self._seconds_to_hhmmss(int(float(value)))
except (TypeError, ValueError):
return None
return value
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
if self.entity_description.key == "current_status":
material_type = self.state_data.get("material_type") or "Filament"
return {"material_type": material_type}
if self.entity_description.value_key == "latest_available_version":
update_info = self.state_data.get("update_info")
if isinstance(update_info, dict):
return {
"current": update_info.get("current"),
"tag": update_info.get("tag"),
"update_available": update_info.get("update_available"),
"download_url": update_info.get("download_url"),
}
return None
if self.entity_description.key == "job_speed_mode":
mode = int(self.state_data.get("print_speed_mode") or 2)
return {
"available_modes": [
{"mode": 1, "description": "Slow"},
{"mode": 2, "description": "Normal"},
{"mode": 3, "description": "Fast"},
],
"print_speed_mode_code": mode,
}
if self.entity_description.value_key not in ("skip_object_count", "skipped_object_count"):
return None
if self.entity_description.key in {"target_nozzle_temperature", "target_hotbed_temperature"}:
return {
"limit_min": 0,
"limit_max": 400 if self.entity_description.key == "target_nozzle_temperature" else 200,
}
skip_state = self.state_data.get("skip_state")
if not isinstance(skip_state, dict):
return None
if self.entity_description.key == "ace_spools":
slots = self.state_data.get("ams_slots")
return {"slots": slots if isinstance(slots, list) else []}
objects = skip_state.get("objects")
skipped = skip_state.get("skipped")
return {
"objects": objects if isinstance(objects, list) else [],
"skipped": skipped if isinstance(skipped, list) else [],
"filename": skip_state.get("filename"),
"ts": skip_state.get("ts"),
}
class KobraXAceDryerSensor(KobraXEntity, SensorEntity):
"""Per-ACE-unit dryer sensor."""
def __init__(
self,
coordinator,
entry,
ace_id: int,
sensor_type: str, # "status", "humidity", "current_temp", "target_temp", "remaining_time"
) -> None:
if sensor_type == "status":
unique_key = f"ace_{ace_id}_dryer_status"
name = f"ACE {ace_id + 1} Dryer Status"
icon = "mdi:tumble-dryer"
elif sensor_type == "humidity":
unique_key = f"ace_{ace_id}_dryer_humidity"
name = f"ACE {ace_id + 1} Dryer Humidity"
icon = "mdi:water-percent"
elif sensor_type == "current_temp":
unique_key = f"ace_{ace_id}_dryer_current_temp"
name = f"ACE {ace_id + 1} Dryer Current Temperature"
icon = None
elif sensor_type == "target_temp":
unique_key = f"ace_{ace_id}_dryer_target_temp"
name = f"ACE {ace_id + 1} Dryer Target Temperature"
icon = None
else: # remaining_time
unique_key = f"ace_{ace_id}_dryer_remaining_time"
name = f"ACE {ace_id + 1} Dryer Remaining Time"
icon = "mdi:timer-sand"
super().__init__(coordinator, entry, unique_key, name)
self._ace_id = ace_id
self._sensor_type = sensor_type
if icon:
self._attr_icon = icon
if sensor_type == "humidity":
self._attr_native_unit_of_measurement = PERCENTAGE
elif sensor_type in ("current_temp", "target_temp"):
self._attr_device_class = SensorDeviceClass.TEMPERATURE
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
self._attr_suggested_unit_of_measurement = UnitOfTemperature.CELSIUS
elif sensor_type == "remaining_time":
pass # HH:mm:ss format, no unit needed
@staticmethod
def _minutes_to_hhmmss(minutes: int) -> str:
"""Convert minutes to HH:mm:ss format."""
total_seconds = minutes * 60
hours = total_seconds // 3600
remaining_seconds = total_seconds % 3600
mins = remaining_seconds // 60
secs = remaining_seconds % 60
return f"{hours:02d}:{mins:02d}:{secs:02d}"
@property
def native_value(self) -> Any:
drying = self.state_data.get("ace_drying") or {}
# Handle per-unit structure: ace_drying[ace_id] or global structure
if isinstance(drying, dict):
# Try per-unit key first
unit_data = drying.get(self._ace_id)
if isinstance(unit_data, dict):
drying = unit_data
elif self._ace_id == 0 and not unit_data:
# Fall back to global structure for unit 0
pass
else:
# No data for this unit
return None
else:
return None
if self._sensor_type == "status":
status = int(drying.get("status", 0)) if drying.get("status") is not None else 0
return "running" if status else "idle"
elif self._sensor_type == "humidity":
humidity = drying.get("humidity")
return round(float(humidity), 1) if humidity is not None else None
elif self._sensor_type == "current_temp":
current_temp = drying.get("current_temp")
return round(float(current_temp), 1) if current_temp is not None else None
elif self._sensor_type == "target_temp":
target_temp = drying.get("target_temp")
return float(target_temp) if target_temp is not None else None
elif self._sensor_type == "remaining_time":
remain = drying.get("remain_time")
if remain is not None:
try:
minutes = int(remain)
return self._minutes_to_hhmmss(minutes)
except (TypeError, ValueError):
return None
return None
return None
class KobraXFilamentSlotSensor(KobraXEntity, SensorEntity):
def __init__(self, coordinator, entry, slot_index: int, field: str) -> None:
name_suffix = {
"color": f"Filament Slot {slot_index + 1} Color",
"type": f"Filament Slot {slot_index + 1} Type",
}[field]
super().__init__(coordinator, entry, f"filament_slot_{slot_index + 1}_{field}", name_suffix)
def __init__(self, coordinator, entry, slot_index: int) -> None:
super().__init__(coordinator, entry, f"slot_{slot_index + 1}", f"Slot {slot_index + 1}")
self._slot_index = slot_index
self._field = field
if field == "color":
self._attr_icon = "mdi:palette"
elif field == "type":
self._attr_icon = "mdi:label"
else:
self._attr_icon = "mdi:numeric"
self._attr_icon = "mdi:circle"
def _slot(self) -> dict[str, Any]:
slots = self.state_data.get("ams_slots") or []
@@ -274,6 +358,9 @@ class KobraXFilamentSlotSensor(KobraXEntity, SensorEntity):
slot = slots[self._slot_index]
return slot if isinstance(slot, dict) else {}
def _slot_limit_for_mode(self) -> int:
return _detected_slot_limit(self.state_data)
@staticmethod
def _to_color_hex(color: Any) -> str | None:
if isinstance(color, list) and len(color) >= 3:
@@ -289,30 +376,155 @@ class KobraXFilamentSlotSensor(KobraXEntity, SensorEntity):
@property
def available(self) -> bool:
return bool(self._slot()) and super().available
return self._slot_index < self._slot_limit_for_mode() and bool(self._slot()) and super().available
@property
def native_value(self) -> Any:
slot = self._slot()
if not slot:
return "EMPTY"
status = slot.get("status")
if status is not None:
try:
if int(status) != 5:
return "EMPTY"
except (TypeError, ValueError):
return "EMPTY"
material = slot.get("type")
material_str = str(material).upper() if material else "EMPTY"
color_hex = self._to_color_hex(slot.get("color"))
if color_hex and material_str != "EMPTY":
return f"{material_str} ({color_hex})"
return material_str
@property
def icon_color(self) -> str | None:
slot = self._slot()
if not slot:
return None
if self._field == "color":
return self._to_color_hex(slot.get("color"))
if self._field == "type":
material = slot.get("type")
return str(material).upper() if material else None
return None
return self._to_color_hex(slot.get("color"))
@property
def entity_picture(self) -> str | None:
"""Return a colored circle picture as a frontend fallback when icon tinting is ignored."""
slot = self._slot()
empty_svg = (
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>"
"<circle cx='32' cy='32' r='28' fill='none' stroke='#666' stroke-width='4'/></svg>"
)
if not slot:
return f"data:image/svg+xml;utf8,{quote(empty_svg)}"
status = slot.get("status")
if status is not None:
try:
if int(status) != 5:
return f"data:image/svg+xml;utf8,{quote(empty_svg)}"
except (TypeError, ValueError):
return f"data:image/svg+xml;utf8,{quote(empty_svg)}"
color_hex = self._to_color_hex(slot.get("color"))
if not color_hex:
return f"data:image/svg+xml;utf8,{quote(empty_svg)}"
svg = (
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>"
"<circle cx='32' cy='32' r='28' fill='"
f"{color_hex}"
"' stroke='#222' stroke-width='4'/></svg>"
)
return f"data:image/svg+xml;utf8,{quote(svg)}"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
slot = self._slot()
if not slot:
return None
return {
"color_hex": self._to_color_hex(slot.get("color")),
"status": slot.get("status"),
"box_id": slot.get("box_id"),
"global_index": slot.get("global_index"),
"activity": slot.get("activity"),
}
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
slots = coordinator.data.get("ams_slots") if coordinator.data else []
slot_count = len(slots) if isinstance(slots, list) and len(slots) > 0 else 4
filament_entities: list[SensorEntity] = []
for slot_index in range(slot_count):
for field in ("color", "type"):
filament_entities.append(KobraXFilamentSlotSensor(coordinator, entry, slot_index, field))
for slot_index in range(MAX_FILAMENT_SLOTS):
filament_entities.append(KobraXFilamentSlotSensor(coordinator, entry, slot_index))
ace_dryer_entities: list[SensorEntity] = []
for ace_id in range(MAX_ACE_UNITS):
for sensor_type in ("status", "humidity", "current_temp", "target_temp", "remaining_time"):
ace_dryer_entities.append(KobraXAceDryerSensor(coordinator, entry, ace_id, sensor_type))
entity_registry = er.async_get(hass)
slot_registry_state: dict[str, int | None] = {"last_limit": None, "short_count": 0}
# Remove legacy single-unit ACE dryer sensors that were replaced by per-unit entities.
legacy_ace_sensor_keys = (
"ace_dryer_status",
"ace_dryer_humidity",
"ace_dryer_current_temp",
"ace_dryer_target_temp",
"ace_dryer_remaining_time",
)
for key in legacy_ace_sensor_keys:
legacy_unique_id = f"{entry.entry_id}_{key}"
legacy_entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, legacy_unique_id)
if legacy_entity_id:
entity_registry.async_remove(legacy_entity_id)
@callback
def _sync_slot_registry_state() -> None:
state_data = coordinator.data or {}
detected_limit = _detected_slot_limit(state_data)
last_limit = slot_registry_state["last_limit"]
short_count = int(slot_registry_state["short_count"] or 0)
if last_limit is None or detected_limit >= last_limit:
effective_limit = detected_limit
slot_registry_state["last_limit"] = detected_limit
slot_registry_state["short_count"] = 0
else:
short_count += 1
slot_registry_state["short_count"] = short_count
if short_count < 2:
effective_limit = last_limit
else:
effective_limit = detected_limit
slot_registry_state["last_limit"] = detected_limit
slot_registry_state["short_count"] = 0
for slot_index in range(MAX_FILAMENT_SLOTS):
entities_to_sync = [
("sensor", f"{entry.entry_id}_slot_{slot_index + 1}"),
("select", f"{entry.entry_id}_slot_{slot_index + 1}_filament_profile"),
]
for platform, unique_id in entities_to_sync:
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
if not entity_id:
continue
reg_entry = entity_registry.async_get(entity_id)
if reg_entry is None:
continue
should_enable = slot_index < effective_limit
if should_enable:
if reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION:
entity_registry.async_update_entity(entity_id, disabled_by=None)
else:
if reg_entry.disabled_by is None:
entity_registry.async_update_entity(
entity_id,
disabled_by=RegistryEntryDisabler.INTEGRATION,
)
async_add_entities(
[
@@ -320,4 +532,68 @@ async def async_setup_entry(hass, entry, async_add_entities):
for description in SENSORS
]
+ filament_entities
+ ace_dryer_entities
)
@callback
def _sync_ace_registry_state() -> None:
state_data = coordinator.data or {}
enabled_ace_units = _detected_ace_unit_count(state_data)
# Define all ACE entity patterns: (platform, unique_id_pattern_parts)
ace_entities: list[tuple[str, str]] = []
for ace_index in range(MAX_ACE_UNITS):
# Switch entities
ace_entities.append(("switch", f"{entry.entry_id}_ace_{ace_index}_auto_feed"))
ace_entities.append(("switch", f"{entry.entry_id}_ace_{ace_index}_dryer"))
# Number entities (temp + duration)
ace_entities.append(("number", f"{entry.entry_id}_ace_{ace_index}_dry_target_temp"))
ace_entities.append(("number", f"{entry.entry_id}_ace_{ace_index}_dry_duration"))
# Button entities (start + stop)
ace_entities.append(("button", f"{entry.entry_id}_ace_{ace_index}_dry_start"))
ace_entities.append(("button", f"{entry.entry_id}_ace_{ace_index}_dry_stop"))
# Sensor entities (status, humidity, current_temp, target_temp, remaining_time)
ace_entities.append(("sensor", f"{entry.entry_id}_ace_{ace_index}_dryer_status"))
ace_entities.append(("sensor", f"{entry.entry_id}_ace_{ace_index}_dryer_humidity"))
ace_entities.append(("sensor", f"{entry.entry_id}_ace_{ace_index}_dryer_current_temp"))
ace_entities.append(("sensor", f"{entry.entry_id}_ace_{ace_index}_dryer_target_temp"))
ace_entities.append(("sensor", f"{entry.entry_id}_ace_{ace_index}_dryer_remaining_time"))
for platform, unique_id in ace_entities:
entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
if not entity_id:
continue
reg_entry = entity_registry.async_get(entity_id)
if reg_entry is None:
continue
# Extract ace_index from unique_id
parts = unique_id.split("_")
if len(parts) >= 3:
try:
ace_index = int(parts[2])
except (ValueError, IndexError):
continue
else:
continue
should_enable = ace_index < enabled_ace_units
if should_enable:
if reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION:
entity_registry.async_update_entity(entity_id, disabled_by=None)
else:
if reg_entry.disabled_by is None:
entity_registry.async_update_entity(
entity_id,
disabled_by=RegistryEntryDisabler.INTEGRATION,
)
entry.async_on_unload(coordinator.async_add_listener(_sync_slot_registry_state))
entry.async_on_unload(coordinator.async_add_listener(_sync_ace_registry_state))
_sync_slot_registry_state()
_sync_ace_registry_state()

View File

@@ -1,153 +0,0 @@
from __future__ import annotations
from typing import Any
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .api import KobraXApiError
from .const import DOMAIN
SERVICE_CHANGE_PRINT_SPEED_MODE = "change_print_speed_mode"
SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE = "change_print_target_nozzle_temperature"
SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE = "change_print_target_hotbed_temperature"
ATTR_CONFIG_ENTRY = "config_entry"
ATTR_DEVICE_ID = "device_id"
ATTR_PRINTER_ID = "printer_id"
ATTR_SPEED_MODE = "speed_mode"
ATTR_TEMPERATURE = "temperature"
SERVICE_SCHEMAS: dict[str, vol.Schema] = {
SERVICE_CHANGE_PRINT_SPEED_MODE: vol.Schema(
{
vol.Optional(ATTR_CONFIG_ENTRY): cv.string,
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_PRINTER_ID): vol.Any(cv.positive_int, cv.string),
vol.Required(ATTR_SPEED_MODE): vol.All(vol.Coerce(int), vol.Range(min=1, max=3)),
}
),
SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE: vol.Schema(
{
vol.Optional(ATTR_CONFIG_ENTRY): cv.string,
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_PRINTER_ID): vol.Any(cv.positive_int, cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0, max=400)),
}
),
SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE: vol.Schema(
{
vol.Optional(ATTR_CONFIG_ENTRY): cv.string,
vol.Optional(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_PRINTER_ID): vol.Any(cv.positive_int, cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=0, max=200)),
}
),
}
def _resolve_entry_id(hass: HomeAssistant, call: ServiceCall) -> str:
requested = call.data.get(ATTR_CONFIG_ENTRY)
domain_data = hass.data.get(DOMAIN, {})
if requested:
if requested in domain_data:
return requested
raise ServiceValidationError(f"Unknown config_entry for {DOMAIN}: {requested}")
entry_ids = [entry_id for entry_id, value in domain_data.items() if isinstance(value, dict) and "api" in value]
if len(entry_ids) == 1:
return entry_ids[0]
raise ServiceValidationError(
"Multiple Kobra X LAN entries loaded. Include config_entry in the service call."
)
async def _handle_change_print_speed_mode(hass: HomeAssistant, call: ServiceCall) -> None:
entry_id = _resolve_entry_id(hass, call)
api = hass.data[DOMAIN][entry_id]["api"]
coordinator = hass.data[DOMAIN][entry_id]["coordinator"]
mode = int(call.data[ATTR_SPEED_MODE])
try:
await api.async_set_speed_mode(mode)
await coordinator.async_request_refresh()
except KobraXApiError as err:
raise HomeAssistantError(str(err)) from err
async def _handle_change_print_target_nozzle_temperature(hass: HomeAssistant, call: ServiceCall) -> None:
entry_id = _resolve_entry_id(hass, call)
api = hass.data[DOMAIN][entry_id]["api"]
coordinator = hass.data[DOMAIN][entry_id]["coordinator"]
temperature = float(call.data[ATTR_TEMPERATURE])
try:
await api.async_set_temperature(nozzle=temperature, bed=None)
await coordinator.async_request_refresh()
except KobraXApiError as err:
raise HomeAssistantError(str(err)) from err
async def _handle_change_print_target_hotbed_temperature(hass: HomeAssistant, call: ServiceCall) -> None:
entry_id = _resolve_entry_id(hass, call)
api = hass.data[DOMAIN][entry_id]["api"]
coordinator = hass.data[DOMAIN][entry_id]["coordinator"]
temperature = float(call.data[ATTR_TEMPERATURE])
try:
await api.async_set_temperature(nozzle=None, bed=temperature)
await coordinator.async_request_refresh()
except KobraXApiError as err:
raise HomeAssistantError(str(err)) from err
def async_register_services(hass: HomeAssistant) -> None:
async def _service_change_print_speed_mode(call: ServiceCall) -> None:
await _handle_change_print_speed_mode(hass, call)
async def _service_change_print_target_nozzle_temperature(call: ServiceCall) -> None:
await _handle_change_print_target_nozzle_temperature(hass, call)
async def _service_change_print_target_hotbed_temperature(call: ServiceCall) -> None:
await _handle_change_print_target_hotbed_temperature(hass, call)
if not hass.services.has_service(DOMAIN, SERVICE_CHANGE_PRINT_SPEED_MODE):
hass.services.async_register(
DOMAIN,
SERVICE_CHANGE_PRINT_SPEED_MODE,
_service_change_print_speed_mode,
schema=SERVICE_SCHEMAS[SERVICE_CHANGE_PRINT_SPEED_MODE],
)
if not hass.services.has_service(DOMAIN, SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE):
hass.services.async_register(
DOMAIN,
SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE,
_service_change_print_target_nozzle_temperature,
schema=SERVICE_SCHEMAS[SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE],
)
if not hass.services.has_service(DOMAIN, SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE):
hass.services.async_register(
DOMAIN,
SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE,
_service_change_print_target_hotbed_temperature,
schema=SERVICE_SCHEMAS[SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE],
)
def async_unregister_services(hass: HomeAssistant) -> None:
for service_name in (
SERVICE_CHANGE_PRINT_SPEED_MODE,
SERVICE_CHANGE_PRINT_TARGET_NOZZLE_TEMPERATURE,
SERVICE_CHANGE_PRINT_TARGET_HOTBED_TEMPERATURE,
):
if hass.services.has_service(DOMAIN, service_name):
hass.services.async_remove(DOMAIN, service_name)

View File

@@ -1,71 +0,0 @@
change_print_speed_mode:
fields:
config_entry:
required: false
selector:
config_entry:
integration: kobrax_lan
device_id:
required: false
selector:
text:
printer_id:
required: false
selector:
text:
speed_mode:
required: true
selector:
number:
min: 1
max: 3
step: 1
mode: box
change_print_target_nozzle_temperature:
fields:
config_entry:
required: false
selector:
config_entry:
integration: kobrax_lan
device_id:
required: false
selector:
text:
printer_id:
required: false
selector:
text:
temperature:
required: true
selector:
number:
min: 0
max: 400
step: 1
mode: box
change_print_target_hotbed_temperature:
fields:
config_entry:
required: false
selector:
config_entry:
integration: kobrax_lan
device_id:
required: false
selector:
text:
printer_id:
required: false
selector:
text:
temperature:
required: true
selector:
number:
min: 0
max: 200
step: 1
mode: box

View File

@@ -0,0 +1,224 @@
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity import EntityCategory
from .api import KobraXApiError
from .const import DOMAIN
from .entity import KobraXEntity
class KobraXAceAutoFeedSwitch(KobraXEntity, SwitchEntity):
def __init__(self, coordinator, entry, ace_id: int) -> None:
super().__init__(coordinator, entry, f"ace_{ace_id}_auto_feed", f"ACE {ace_id + 1} Auto Fill")
self._ace_id = ace_id
self._attr_icon = "mdi:autorenew"
@property
def is_on(self) -> bool:
auto_feed = self.state_data.get("ace_auto_feed") or {}
if not isinstance(auto_feed, dict):
return False
value = auto_feed.get(self._ace_id)
if value is None:
value = auto_feed.get(str(self._ace_id))
return bool(value)
def _apply_optimistic_state(self, is_on: bool) -> None:
merged: dict[str, Any] = dict(self.coordinator.data or {})
auto_feed = merged.get("ace_auto_feed")
if not isinstance(auto_feed, dict):
auto_feed = {}
else:
auto_feed = dict(auto_feed)
key: int | str = self._ace_id
if key not in auto_feed and str(self._ace_id) in auto_feed:
key = str(self._ace_id)
auto_feed[key] = 1 if is_on else 0
merged["ace_auto_feed"] = auto_feed
self.coordinator.async_set_updated_data(merged)
async def async_turn_on(self, **kwargs) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
await api.async_set_ace_auto_feed(self._ace_id, True)
self._apply_optimistic_state(True)
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:
super().__init__(coordinator, entry, key, name)
self._setting_key = setting_key
self._attr_icon = icon
self._attr_entity_category = EntityCategory.CONFIG
def _current_settings(self) -> dict[str, Any]:
settings = self.state_data.get("settings")
return settings if isinstance(settings, dict) else {}
@property
def available(self) -> bool:
return bool(self._current_settings()) and super().available
@property
def is_on(self) -> bool:
value = self._current_settings().get(self._setting_key)
try:
return bool(int(value))
except (TypeError, ValueError):
return bool(value)
def _apply_optimistic_state(self, is_on: bool) -> None:
merged: dict[str, Any] = dict(self.coordinator.data or {})
settings = merged.get("settings")
if not isinstance(settings, dict):
settings = {}
else:
settings = dict(settings)
settings[self._setting_key] = 1 if is_on else 0
merged["settings"] = settings
self.coordinator.async_set_updated_data(merged)
async def _set_state(self, is_on: bool) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
settings = await api.async_get_settings()
settings[self._setting_key] = 1 if is_on else 0
await api.async_set_settings(settings)
self._apply_optimistic_state(is_on)
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
async def async_turn_on(self, **kwargs) -> None:
await self._set_state(True)
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:
super().__init__(coordinator, entry, f"ace_{ace_id}_dryer", f"ACE {ace_id + 1} Dryer")
self._ace_id = ace_id
self._attr_icon = "mdi:tumble-dryer"
@property
def is_on(self) -> bool:
drying = self.state_data.get("ace_drying") or {}
if not isinstance(drying, dict):
return False
unit_data = drying.get(self._ace_id)
if unit_data is None:
unit_data = drying.get(str(self._ace_id))
if isinstance(unit_data, dict):
status = unit_data.get("status")
elif self._ace_id == 0:
# Backward-compatible fallback: older bridge payloads may expose unit 0 as a flat object.
status = drying.get("status")
else:
status = None
try:
return int(status) > 0 if status is not None else False
except (TypeError, ValueError):
return False
def _apply_optimistic_state(self, is_on: bool) -> None:
merged: dict[str, Any] = dict(self.coordinator.data or {})
drying = merged.get("ace_drying")
if not isinstance(drying, dict):
drying = {}
else:
drying = dict(drying)
unit_data = drying.get(self._ace_id)
unit_key: int | str = self._ace_id
if unit_data is None:
unit_data = drying.get(str(self._ace_id))
if unit_data is not None:
unit_key = str(self._ace_id)
if isinstance(unit_data, dict):
next_unit_data = dict(unit_data)
else:
next_unit_data = {}
next_unit_data["status"] = 1 if is_on else 0
drying[unit_key] = next_unit_data
# Keep backward-compatible flat status for unit 0 payload variants.
if self._ace_id == 0:
drying["status"] = 1 if is_on else 0
merged["ace_drying"] = drying
self.coordinator.async_set_updated_data(merged)
async def async_turn_on(self, **kwargs) -> None:
api = self.hass.data[DOMAIN][self._entry.entry_id]["api"]
try:
cfg = self.hass.data[DOMAIN][self._entry.entry_id]["ace_dry_config"]
ace_cfg = cfg.get(self._ace_id) or {}
await api.async_set_ace_dry(
"start",
target_temp=int(ace_cfg.get("target_temp", 45)),
duration=int(ace_cfg.get("duration", 240)),
ace_id=self._ace_id,
)
self._apply_optimistic_state(True)
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_dry("stop", ace_id=self._ace_id)
self._apply_optimistic_state(False)
except KobraXApiError as err:
raise ServiceValidationError(str(err)) from err
async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"]
entities = [
KobraXBridgeSettingSwitch(
coordinator,
entry,
"camera_on_print",
"Camera On Print",
"camera_on_print",
"mdi:camera-wireless",
),
KobraXBridgeSettingSwitch(
coordinator,
entry,
"web_upload_warning",
"Web Upload Warning",
"web_upload_warning",
"mdi:alert-outline",
),
]
# Pre-create switches for all 4 possible ACE units
entities.extend(KobraXAceAutoFeedSwitch(coordinator, entry, ace_id) for ace_id in range(4))
entities.extend(KobraXAceDryerSwitch(coordinator, entry, ace_id) for ace_id in range(4))
async_add_entities(entities)

View File

@@ -4,7 +4,7 @@
"domains": [
"kobrax_lan"
],
"homeassistant": "2024.6.0",
"homeassistant": "2026.3.0",
"iot_class": "local_polling",
"render_readme": true
}