mirror of
https://github.com/gangoke/kobrax-lan-hass-component.git
synced 2026-06-10 05:02:12 +02:00
Compare commits
5 Commits
v0.9.18
...
card_front
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a73fafe1f0 | ||
|
|
894f1fad22 | ||
|
|
35091826a4 | ||
|
|
f675c8f456 | ||
|
|
a65b58798c |
69
.github/workflows/build-card.yml
vendored
Normal file
69
.github/workflows/build-card.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
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
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
|
||||
86
README.md
86
README.md
@@ -1,62 +1,62 @@
|
||||
# Kobra X LAN for Home Assistant
|
||||
# Kobra X Home Assistant Component
|
||||
|
||||
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.
|
||||
Home Assistant HACS integration for controlling and monitoring an Anycubic Kobra X through KX-Bridge.
|
||||
|
||||
Architecture:
|
||||
|
||||
- printer <-> [KX-Bridge](https://gitea.it-drui.de/viewit/KX-Bridge-Release) <-> this integration <-> Home Assistant
|
||||
- printer <-> KX-Bridge-Release <-> this integration <-> Home Assistant
|
||||
|
||||
## Requirements
|
||||
## Features
|
||||
|
||||
- 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`
|
||||
- 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`)
|
||||
|
||||
## Installation
|
||||
## Prerequisites
|
||||
|
||||
### Option 1: HACS
|
||||
1. KX-Bridge must be running and reachable from Home Assistant.
|
||||
2. Verify KX-Bridge is accessible at `http://<bridge-host>:7125`.
|
||||
|
||||
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.
|
||||
## Installation (HACS)
|
||||
|
||||
1. Add this repository as a custom repository in HACS with category `Integration`.
|
||||
2. Install the integration.
|
||||
3. Restart Home Assistant.
|
||||
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.
|
||||
4. Add integration `Kobra X` 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 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.
|
||||
- Printer name: Friendly display name
|
||||
|
||||
## Notes
|
||||
|
||||
- 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).
|
||||
- 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.
|
||||
|
||||
@@ -9,6 +9,7 @@ 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__)
|
||||
|
||||
@@ -34,26 +35,10 @@ 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
|
||||
|
||||
@@ -62,4 +47,6 @@ 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
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
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."""
|
||||
|
||||
@@ -24,21 +20,6 @@ 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:
|
||||
@@ -61,18 +42,6 @@ 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", [])
|
||||
@@ -80,109 +49,6 @@ 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", {})
|
||||
|
||||
@@ -213,24 +79,9 @@ 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")
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import KobraXEntity
|
||||
@@ -24,7 +23,6 @@ BINARY_SENSORS: tuple[KobraXBinaryDescription, ...] = (
|
||||
name="Online",
|
||||
value_key="kobra_state",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
KobraXBinaryDescription(
|
||||
key="printing",
|
||||
@@ -38,6 +36,12 @@ 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.
|
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.8 KiB |
@@ -4,7 +4,6 @@ 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
|
||||
@@ -14,7 +13,6 @@ 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, ...] = (
|
||||
@@ -41,7 +39,6 @@ BUTTONS: tuple[KobraXButtonDescription, ...] = (
|
||||
name="Connect Bridge",
|
||||
icon="mdi:lan-connect",
|
||||
action="connect",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
KobraXButtonDescription(
|
||||
@@ -49,29 +46,7 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -83,25 +58,6 @@ 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:
|
||||
@@ -115,25 +71,16 @@ 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"]
|
||||
|
||||
entities = [
|
||||
KobraXActionButton(coordinator, entry, description)
|
||||
for description in BUTTONS
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
[
|
||||
KobraXActionButton(coordinator, entry, description)
|
||||
for description in BUTTONS
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
|
||||
from .api import KobraXApiError
|
||||
@@ -22,27 +21,14 @@ 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_h264_proxy_url": api.camera_h264_proxy_url(),
|
||||
"camera_mjpeg_proxy_url": api.camera_stream_proxy_url(),
|
||||
"camera_mjpeg_proxy_url": self.hass.data[DOMAIN][self._entry.entry_id]["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:
|
||||
@@ -50,14 +36,6 @@ 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
|
||||
@@ -65,12 +43,9 @@ class KobraXCamera(KobraXEntity, Camera):
|
||||
try:
|
||||
camera_url = await api.async_get_camera_url()
|
||||
except KobraXApiError:
|
||||
return selected_url
|
||||
return api.camera_stream_proxy_url()
|
||||
|
||||
if isinstance(camera_url, str) and camera_url:
|
||||
selected_url = camera_url
|
||||
|
||||
return selected_url
|
||||
return camera_url or api.camera_stream_proxy_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"]
|
||||
|
||||
@@ -10,8 +10,7 @@ CONF_PRINTER_NAME = "printer_name"
|
||||
DEFAULT_HOST = "localhost:7125"
|
||||
DEFAULT_PRINTER_NAME = "Anycubic Kobra X"
|
||||
|
||||
UPDATE_INTERVAL = timedelta(seconds=3)
|
||||
UPDATE_CHECK_INTERVAL = timedelta(hours=1)
|
||||
UPDATE_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
PLATFORMS = [
|
||||
"sensor",
|
||||
@@ -19,8 +18,6 @@ PLATFORMS = [
|
||||
"light",
|
||||
"select",
|
||||
"button",
|
||||
"switch",
|
||||
"number",
|
||||
"camera",
|
||||
"image",
|
||||
]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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_CHECK_INTERVAL, UPDATE_INTERVAL
|
||||
from .const import DOMAIN, UPDATE_INTERVAL
|
||||
|
||||
|
||||
class KobraXCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
@@ -19,76 +18,9 @@ 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:
|
||||
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
|
||||
return await self.api.async_get_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
|
||||
|
||||
138
custom_components/kobrax_lan/frontend_panel/.eslintrc.js
Normal file
138
custom_components/kobrax_lan/frontend_panel/.eslintrc.js
Normal file
@@ -0,0 +1,138 @@
|
||||
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"
|
||||
}
|
||||
};
|
||||
2
custom_components/kobrax_lan/frontend_panel/.gitignore
vendored
Normal file
2
custom_components/kobrax_lan/frontend_panel/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
25
custom_components/kobrax_lan/frontend_panel/.prettierrc
Normal file
25
custom_components/kobrax_lan/frontend_panel/.prettierrc
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.ts,*.js",
|
||||
"options": {
|
||||
"htmlWhitespaceSensitivity": "strict"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.scss",
|
||||
"options": {
|
||||
"parser": "scss",
|
||||
"singleQuote": true,
|
||||
"printWidth": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"printWidth": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
38
custom_components/kobrax_lan/frontend_panel/README.md
Normal file
38
custom_components/kobrax_lan/frontend_panel/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
4801
custom_components/kobrax_lan/frontend_panel/package-lock.json
generated
Normal file
4801
custom_components/kobrax_lan/frontend_panel/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
custom_components/kobrax_lan/frontend_panel/package.json
Normal file
57
custom_components/kobrax_lan/frontend_panel/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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',
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
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',
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
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"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,451 @@
|
||||
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%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,762 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
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}>×</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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
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}>×</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}
|
||||
>
|
||||
|
||||
</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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
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",
|
||||
});
|
||||
@@ -0,0 +1,396 @@
|
||||
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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,961 @@
|
||||
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}>×</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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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%);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,284 @@
|
||||
/* 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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
7
custom_components/kobrax_lan/frontend_panel/src/const.ts
Normal file
7
custom_components/kobrax_lan/frontend_panel/src/const.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
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"];
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
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 };
|
||||
840
custom_components/kobrax_lan/frontend_panel/src/helpers.ts
Normal file
840
custom_components/kobrax_lan/frontend_panel/src/helpers.ts
Normal file
@@ -0,0 +1,840 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* 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);
|
||||
@@ -0,0 +1,171 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// 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>⯅</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);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,61 @@
|
||||
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>`;
|
||||
@@ -0,0 +1,478 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* 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?.();
|
||||
}
|
||||
};
|
||||
466
custom_components/kobrax_lan/frontend_panel/src/types.ts
Normal file
466
custom_components/kobrax_lan/frontend_panel/src/types.ts
Normal file
@@ -0,0 +1,466 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,170 @@
|
||||
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}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,146 @@
|
||||
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}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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";
|
||||
}
|
||||
25
custom_components/kobrax_lan/frontend_panel/tsconfig.json
Normal file
25
custom_components/kobrax_lan/frontend_panel/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,8 @@
|
||||
"@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.3.0"
|
||||
"version": "0.1.0"
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
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)
|
||||
@@ -1,34 +1,13 @@
|
||||
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)"]
|
||||
|
||||
@@ -55,139 +34,8 @@ 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(
|
||||
entities
|
||||
[KobraXPrintSpeedSelect(coordinator, entry, "print_speed_mode", "Print Speed")]
|
||||
)
|
||||
|
||||
@@ -2,45 +2,14 @@ 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
|
||||
@@ -128,41 +97,105 @@ 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="skip_object_count",
|
||||
name="Skip Object Count",
|
||||
value_key="skip_object_count",
|
||||
icon="mdi:vector-polygon",
|
||||
key="job_state",
|
||||
name="Job State",
|
||||
value_key="print_state",
|
||||
icon="mdi:state-machine",
|
||||
),
|
||||
KobraXSensorDescription(
|
||||
key="skipped_object_count",
|
||||
name="Skipped Object Count",
|
||||
value_key="skipped_object_count",
|
||||
icon="mdi:content-cut",
|
||||
key="current_status",
|
||||
name="Current Status",
|
||||
value_key="print_state",
|
||||
icon="mdi:information-outline",
|
||||
),
|
||||
KobraXSensorDescription(
|
||||
key="filament_mode",
|
||||
name="Filament Mode",
|
||||
value_key="filament_mode",
|
||||
icon="mdi:shape-outline",
|
||||
key="job_progress",
|
||||
name="Job Progress",
|
||||
value_key="progress",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:percent",
|
||||
),
|
||||
KobraXSensorDescription(
|
||||
key="ace_unit_count",
|
||||
name="ACE Unit Count",
|
||||
value_key="ace_unit_count",
|
||||
icon="mdi:package-variant",
|
||||
key="job_time_elapsed",
|
||||
name="Job Time Elapsed",
|
||||
value_key="print_duration",
|
||||
icon="mdi:timer-outline",
|
||||
),
|
||||
KobraXSensorDescription(
|
||||
key="bridge_version",
|
||||
name="Bridge Version",
|
||||
value_key="version",
|
||||
icon="mdi:source-branch",
|
||||
key="job_time_remaining",
|
||||
name="Job Time Remaining",
|
||||
value_key="remain_time",
|
||||
icon="mdi:timer-sand",
|
||||
),
|
||||
KobraXSensorDescription(
|
||||
key="latest_available_version",
|
||||
name="Latest Available Version",
|
||||
value_key="latest_available_version",
|
||||
icon="mdi:cloud-download-outline",
|
||||
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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -173,183 +206,66 @@ 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.value_key == "progress" and value is not None:
|
||||
if self.entity_description.key in {"progress", "job_progress"} and value is not None:
|
||||
return round(float(value) * 100, 1)
|
||||
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
|
||||
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"
|
||||
return value
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
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 == "current_status":
|
||||
material_type = self.state_data.get("material_type") or "Filament"
|
||||
return {"material_type": material_type}
|
||||
|
||||
if self.entity_description.value_key not in ("skip_object_count", "skipped_object_count"):
|
||||
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,
|
||||
}
|
||||
|
||||
skip_state = self.state_data.get("skip_state")
|
||||
if not isinstance(skip_state, dict):
|
||||
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,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
if self.entity_description.key == "ace_spools":
|
||||
slots = self.state_data.get("ams_slots")
|
||||
return {"slots": slots if isinstance(slots, list) else []}
|
||||
|
||||
|
||||
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) -> None:
|
||||
super().__init__(coordinator, entry, f"slot_{slot_index + 1}", f"Slot {slot_index + 1}")
|
||||
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)
|
||||
self._slot_index = slot_index
|
||||
self._attr_icon = "mdi:circle"
|
||||
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"
|
||||
|
||||
def _slot(self) -> dict[str, Any]:
|
||||
slots = self.state_data.get("ams_slots") or []
|
||||
@@ -358,9 +274,6 @@ 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:
|
||||
@@ -376,155 +289,30 @@ class KobraXFilamentSlotSensor(KobraXEntity, SensorEntity):
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return self._slot_index < self._slot_limit_for_mode() and bool(self._slot()) and super().available
|
||||
return 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
|
||||
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"),
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
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(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,
|
||||
)
|
||||
for slot_index in range(slot_count):
|
||||
for field in ("color", "type"):
|
||||
filament_entities.append(KobraXFilamentSlotSensor(coordinator, entry, slot_index, field))
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
@@ -532,68 +320,4 @@ 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()
|
||||
|
||||
153
custom_components/kobrax_lan/services.py
Normal file
153
custom_components/kobrax_lan/services.py
Normal file
@@ -0,0 +1,153 @@
|
||||
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)
|
||||
71
custom_components/kobrax_lan/services.yaml
Normal file
71
custom_components/kobrax_lan/services.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
@@ -1,224 +0,0 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user