6 Commits

Author SHA1 Message Date
Gangoke
a73fafe1f0 Fix build-card workflow token guard 2026-05-17 02:48:37 -10:00
Gangoke
894f1fad22 Use GitHub owner for card dispatch 2026-05-17 02:47:34 -10:00
Gangoke
35091826a4 Pass source branch to card sync 2026-05-17 02:43:28 -10:00
Gangoke
f675c8f456 Add card artifact sync dispatch 2026-05-17 02:39:16 -10:00
Gangoke
a65b58798c Add new features and services for KobraX LAN integration
- Implemented frontend components for file management including local, cloud, and udisk file views.
- Created a main view for printer status and information display.
- Added print functionality with options for saving in cloud and without cloud save.
- Introduced new services for changing print speed mode and target temperatures for nozzle and hotbed.
- Established TypeScript configuration for better development experience.
- Enhanced styling for improved UI consistency across components.
2026-05-17 02:17:23 -10:00
gangoke
3f3e4f534d Merge pull request 'filament entities' (#1) from camera-rework into main
Reviewed-on: https://gitea.gangoke.app/gangoke/kobrax-lan-hass-component/pulls/1
2026-05-17 11:01:53 +00:00
66 changed files with 16640 additions and 1 deletions

69
.github/workflows/build-card.yml vendored Normal file
View 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
View File

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

View File

@@ -12,9 +12,11 @@ Architecture:
- 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`)
## Prerequisites
@@ -40,3 +42,21 @@ The config flow asks for:
- 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.

View 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__)
@@ -36,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"entry": entry,
}
async_register_services(hass)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -44,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

View File

@@ -36,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,
),
)

View 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"
}
};

View File

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

View 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
}
}
]
}

View 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.

View File

@@ -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": {}
}
}
}

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@@ -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',
};

View File

@@ -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',
};

View File

@@ -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"

View File

@@ -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"

View File

@@ -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%;
}
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this._renderCard()}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return html`
<div>
<div class="ac-drying-header">${this._heading}</div>
<div class="ac-drying-buttonscont">
${this._hasDryingPreset1
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset1}>
${this._buttonTextPreset} 1<br />
${this._dryingPresetDur1} ${this._buttonTextMinutes} @
${this._dryingPresetTemp1}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset2
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset2}>
${this._buttonTextPreset} 2<br />
${this._dryingPresetDur2} ${this._buttonTextMinutes} @
${this._dryingPresetTemp2}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset3
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset3}>
${this._buttonTextPreset} 3<br />
${this._dryingPresetDur3} ${this._buttonTextMinutes} @
${this._dryingPresetTemp3}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingPreset4
? html`
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingPreset4}>
${this._buttonTextPreset} 4<br />
${this._dryingPresetDur4} ${this._buttonTextMinutes} @
${this._dryingPresetTemp4}°C
</ha-control-button>
</div>
`
: nothing}
${this._hasDryingStop
? html`
<div class="ac-flex-break"></div>
<div class="ac-drying-buttoncont">
<ha-control-button @click=${this._handleDryingStop}>
${this._buttonStopDrying}
</ha-control-button>
</div>
`
: nothing}
</div>
</div>
`;
}
private _pressHassButton(suffix: string): void {
if (this.printerEntityIdPart) {
this.hass
.callService("button", "press", {
entity_id: getPrinterEntityId(
this.printerEntityIdPart,
"button",
suffix,
),
})
.then()
.catch((_e: unknown) => {
// Show in error modal
});
}
}
private _handleDryingPreset1 = (): void => {
this._pressHassButton(this._dryingPresetId1);
this._closeModal();
};
private _handleDryingPreset2 = (): void => {
this._pressHassButton(this._dryingPresetId2);
this._closeModal();
};
private _handleDryingPreset3 = (): void => {
this._pressHassButton(this._dryingPresetId3);
this._closeModal();
};
private _handleDryingPreset4 = (): void => {
this._pressHassButton(this._dryingPresetId4);
this._closeModal();
};
private _handleDryingStop = (): void => {
this._pressHassButton(this._dryingStopId);
this._closeModal();
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventDrying>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this.box_id = Number(e.detail.box_id);
}
};
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this.box_id = 0;
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-drying-header {
font-size: 24px;
text-align: center;
font-weight: 600;
}
ha-control-button {
min-width: 150px;
font-size: 14px;
min-height: 55px;
width: 100%;
box-sizing: border-box;
}
.ac-flex-break {
flex-basis: 100%;
height: 0;
}
.ac-drying-buttonscont {
display: flex;
flex-wrap: wrap;
margin-top: 30px;
align-items: center;
justify-content: center;
}
.ac-drying-buttoncont {
width: 50%;
margin: 0;
position: relative;
box-sizing: border-box;
padding: 10px;
}
`;
}
}

View File

@@ -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}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this.color ? this._renderCard() : nothing}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return this.spool_index >= 0
? html`
<div>
<div class="ac-slot-title">
${this._heading}: ${this.spool_index + 1}
</div>
<div>
<div>
<p class="ac-modal-label">${this._labelSelectMaterial}:</p>
<anycubic-ui-select-dropdown
.availableOptions=${AnycubicMaterialType}
.placeholder=${AnycubicMaterialType.PLA}
.initialItem=${this.material_type}
></anycubic-ui-select-dropdown>
</div>
${this._renderPresets()}
<div>
<p class="ac-modal-label">${this._labelSelectColour}:</p>
<color-picker .value=${this.color}></color-picker>
</div>
</div>
<div class="ac-save-settings">
<ha-control-button
.disabled=${this._changingSlot}
@click=${this._handleSaveButton}
>
${this._buttonSave}
</ha-control-button>
</div>
</div>
`
: nothing;
}
private _renderPresets(): LitTemplateResult {
return html`
<div>
<p class="ac-modal-label">Choose Preset Colour:</p>
<div class="ac-mcb-presets">
${this.slotColors
? map(this.slotColors, (preset, _index) => {
const presetStyle = {
"background-color": preset,
};
return html`
<div
class="ac-mcb-preset-color"
style=${styleMap(presetStyle)}
.preset=${preset}
@click=${this._colourPresetChange}
>
&nbsp;
</div>
`;
})
: nothing}
</div>
</div>
`;
}
private _colourPresetChange = (
ev: DomClickEvent<EvtTargColourPreset>,
): void => {
this.color = ev.currentTarget.preset;
if (this._elColorPicker) {
this._elColorPicker.color = this.color;
}
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventSpool>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this.box_id = Number(e.detail.box_id);
this.spool_index = Number(e.detail.spool_index);
this.material_type = materialTypeFromString(e.detail.material_type);
this.color = e.detail.color;
}
};
private _handleDropdownEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<DropdownEvent<string, string>>;
e.stopPropagation();
if (e.detail.value) {
this.material_type = materialTypeFromString(e.detail.value);
}
};
private _handleColourEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ColourPickEvent>;
e.stopPropagation();
if (e.detail.color) {
this.color = e.detail.color.rgb;
}
};
private _handleColourPickEvent = (e: Event): void => {
this._handleColourEvent(e);
if (!this._changingSlot) {
this._submitSlotChanges();
}
};
private _handleSaveButton = (): void => {
this._submitSlotChanges();
};
private _serviceAvailable(serviceName: string): boolean {
return Boolean(this.hass?.services?.[platform]?.[serviceName]);
}
private _submitSlotChanges(): void {
if (
this.selectedPrinterDevice &&
this.material_type &&
this.spool_index >= 0 &&
this.color &&
this.color.length >= 3
) {
const serv = `multi_color_box_set_slot_${this.material_type.toLowerCase()}`;
if (!this._serviceAvailable(serv)) {
this._closeModal();
return;
}
this._changingSlot = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
box_id: this.box_id,
slot_number: this.spool_index + 1,
slot_color_red: this.color[0],
slot_color_green: this.color[1],
slot_color_blue: this.color[2],
})
.then(() => {
this._changingSlot = false;
})
.catch((_e: unknown) => {
this._changingSlot = false;
});
this._closeModal();
}
}
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this.spool_index = -1;
this.material_type = undefined;
this.color = undefined;
this.box_id = 0;
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-slot-title {
font-size: 24px;
text-align: center;
font-weight: 600;
}
.ac-mcb-presets {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.ac-mcb-preset-color {
width: 30px;
height: 30px;
border-radius: 15px;
margin: 20px 10px;
}
ha-control-button {
min-width: 150px;
margin: 30px auto 0px;
font-size: 14px;
}
color-picker {
--font-fam: var(--token-font-family-primary);
--bg-color: var(--ha-card-background);
--label-color: var(--secondary-text-color);
--form-border-color: var(--ha-card-background);
--input-active-border-color: var(--primary-color);
--input-bg: var(--primary-background-color);
--input-active-bg: var(--ha-card-background);
--input-color: var(--secondary-text-color);
--input-active-color: var(--primary-text-color);
--input-active-box-shadow: 0 2px 5px #ccc;
--button-active-bg: var(--state-active-color);
--button-active-color: var(--token-color-icon-primary);
--outer-box-shadow: 0 4px 12px #111;
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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",
});

View File

@@ -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);
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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,
},
},
};

View File

@@ -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}>&times;</span>
<div class="ac-modal-card" @click=${this._cardClick}>
${this._renderCard()}
</div>
</div>
`;
}
_renderCard(): LitTemplateResult {
return this._confirmationType
? this._renderConfirm()
: this._renderSettings();
}
_renderConfirm(): LitTemplateResult {
return html`
<div>
<div class="ac-settings-header">Confirm Action</div>
<div>
<div class="ac-confirm-description">${this._confirmMessage}</div>
<div class="ac-confirm-buttons">
<ha-control-button
@click=${this._handleConfirmApprove}
.disabled=${this._changingSettings}
>
${this._buttonYes}
</ha-control-button>
<ha-control-button @click=${this._handleConfirmCancel}>
${this._buttonNo}
</ha-control-button>
</div>
</div>
</div>
`;
}
_renderSettings(): LitTemplateResult {
return html`
<div>
<div class="ac-settings-header">Print Settings</div>
<div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.PAUSE}
@click=${this._setConfirmationMode}
>
${this._buttonPrintPause}
</ha-control-button>
</div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.RESUME}
@click=${this._setConfirmationMode}
>
${this._buttonPrintResume}
</ha-control-button>
</div>
<div class="ac-settings-row ac-settings-buttonrow">
<ha-control-button
.confirmation_type=${AnycubicPrintOptionConfirmationType.CANCEL}
@click=${this._setConfirmationMode}
>
${this._buttonPrintCancel}
</ha-control-button>
</div>
${this.isFDM
? html`
<div class="ac-settings-row">
<anycubic-ui-select-dropdown
.availableOptions=${this.availableSpeedModes}
.placeholder=${this.currentSpeedModeDescr}
.initialItem=${this.currentSpeedModeDescr}
></anycubic-ui-select-dropdown>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveSpeedModeButton}
>
${this._buttonSaveSpeedMode}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentTargetTempNozzle}
.placeholder=${this.currentTargetTempNozzle}
.label=${this._labelNozzleTemperature}
.type=${"number"}
.min=${this.minTargetTempNozzle}
.max=${this.maxTargetTempNozzle}
@input=${this._handleTargetTempNozzleChange}
@keydown=${this._handleTargetTempNozzleKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveTargetTempNozzleButton}
>
${this._buttonSaveTargetNozzle}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentTargetTempHotbed}
.placeholder=${this.currentTargetTempHotbed}
.label=${this._labelHotbedTemperature}
.type=${"number"}
.min=${this.minTargetTempHotbed}
.max=${this.maxTargetTempHotbed}
@input=${this._handleTargetTempHotbedChange}
@keydown=${this._handleTargetTempHotbedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveTargetTempHotbedButton}
>
${this._buttonSaveTargetHotbed}
</ha-control-button>
</div>
<div class="ac-settings-row">
<ha-textfield
.value=${this.currentFanSpeed}
.placeholder=${this.currentFanSpeed}
.label=${this._labelFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleFanSpeedChange}
@keydown=${this._handleFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveFanSpeedButton}
>
${this._buttonSaveFanSpeed}
</ha-control-button>
</div>
<div class="ac-settings-row ac-disabled-feature">
<ha-textfield
.value=${this.currentAuxFanSpeed}
.placeholder=${this.currentAuxFanSpeed}
.label=${this._labelAuxFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleAuxFanSpeedChange}
@keydown=${this._handleAuxFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveAuxFanSpeedButton}
>
${this._buttonSaveAuxFanSpeed}
</ha-control-button>
</div>
<div class="ac-settings-row ac-disabled-feature">
<ha-textfield
.value=${this.currentBoxFanSpeed}
.placeholder=${this.currentBoxFanSpeed}
.label=${this._labelBoxFanSpeed}
.type=${"number"}
.min=${0}
.max=${100}
@input=${this._handleBoxFanSpeedChange}
@keydown=${this._handleBoxFanSpeedKeyDown}
></ha-textfield>
<ha-control-button
.disabled=${this._changingSettings}
@click=${this._handleSaveBoxFanSpeedButton}
>
${this._buttonSaveBoxFanSpeed}
</ha-control-button>
</div>
`
: nothing}
</div>
</div>
`;
}
private _setConfirmationMode = (
ev: DomClickEvent<EvtTargConfirmationMode>,
): void => {
this._confirmationType = ev.currentTarget.confirmation_type;
this._confirmMessage = localize(
"card.print_settings.confirm_message",
this.language,
"action",
localize(
"common.actions." + (this._confirmationType as string),
this.language,
),
);
};
private _pressHassButton(suffix: string): void {
this._changingSettings = true;
this.hass
.callService("button", "press", {
entity_id: getPrinterEntityId(
this.printerEntityIdPart,
"button",
suffix,
),
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
}
private _handleConfirmApprove = (): void => {
switch (this._confirmationType) {
case AnycubicPrintOptionConfirmationType.PAUSE:
this._pressHassButton("pause_print");
break;
case AnycubicPrintOptionConfirmationType.RESUME:
this._pressHassButton("resume_print");
break;
case AnycubicPrintOptionConfirmationType.CANCEL:
this._pressHassButton("cancel_print");
break;
default:
break;
}
this._confirmationType = undefined;
this._closeModal();
};
private _handleConfirmCancel = (): void => {
this._confirmationType = undefined;
};
private _handleFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentFanSpeed = Number(newSpeed);
this._userEditFanSpeed = true;
};
private _handleAuxFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentAuxFanSpeed = Number(newSpeed);
this._userEditAuxFanSpeed = true;
};
private _handleBoxFanSpeedChange = (ev: Event): void => {
const newSpeed = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentBoxFanSpeed = Number(newSpeed);
this._userEditBoxFanSpeed = true;
};
private _handleFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedFanSpeed();
} else {
this._userEditFanSpeed = true;
}
};
private _handleAuxFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedAuxFanSpeed();
} else {
this._userEditAuxFanSpeed = true;
}
};
private _handleBoxFanSpeedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedBoxFanSpeed();
} else {
this._userEditBoxFanSpeed = true;
}
};
private _handleTargetTempNozzleChange = (ev: Event): void => {
const newTemp = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentTargetTempNozzle = Number(newTemp);
this._userEditTargetTempNozzle = true;
};
private _handleTargetTempHotbedChange = (ev: Event): void => {
const newTemp = (
ev.currentTarget as unknown as TextfieldChangeDetail<number>
).value;
this.currentTargetTempHotbed = Number(newTemp);
this._userEditTargetTempHotbed = true;
};
private _handleTargetTempNozzleKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedTargetTempNozzle();
} else {
this._userEditTargetTempNozzle = true;
}
};
private _handleTargetTempHotbedKeyDown = (ev: KeyboardEvent): void => {
if (ev.code === "Enter") {
ev.preventDefault();
this._submitChangedTargetTempHotbed();
} else {
this._userEditTargetTempHotbed = true;
}
};
private _handleModalEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<ModalEventBase>;
e.stopPropagation();
if (e.detail.modalOpen) {
this._isOpen = true;
this._resetUserEdits();
}
};
private _handleDropdownEvent = (evt: Event): void => {
const e = evt as HASSDomEvent<DropdownEvent<number, string>>;
e.stopPropagation();
this._userEditSpeedMode = true;
if (typeof e.detail.key !== "undefined") {
this.currentSpeedModeKey = e.detail.key;
this.currentSpeedModeDescr =
this.currentSpeedModeKey >= 0 &&
this.currentSpeedModeKey in this.availableSpeedModes
? this.availableSpeedModes[this.currentSpeedModeKey]
: undefined;
}
};
private _handleSaveFanSpeedButton = (): void => {
this._submitChangedFanSpeed();
this._resetUserEdits();
};
private _handleSaveAuxFanSpeedButton = (): void => {
this._submitChangedAuxFanSpeed();
this._resetUserEdits();
};
private _handleSaveBoxFanSpeedButton = (): void => {
this._submitChangedBoxFanSpeed();
this._resetUserEdits();
};
private _handleSaveSpeedModeButton = (): void => {
this._submitChangedSpeedMode();
this._resetUserEdits();
};
private _handleSaveTargetTempNozzleButton = (): void => {
this._submitChangedTargetTempNozzle();
this._resetUserEdits();
};
private _handleSaveTargetTempHotbedButton = (): void => {
this._submitChangedTargetTempHotbed();
this._resetUserEdits();
};
private _resetUserEdits(): void {
this._userEditFanSpeed = false;
this._userEditAuxFanSpeed = false;
this._userEditBoxFanSpeed = false;
this._userEditTargetTempNozzle = false;
this._userEditTargetTempHotbed = false;
this._userEditSpeedMode = false;
}
private _closeModal = (e?: Event | undefined): void => {
if (e) {
e.stopPropagation();
}
this._isOpen = false;
this._resetUserEdits();
};
private _cardClick = (e: Event): void => {
e.stopPropagation();
};
private _serviceAvailable(serviceName: string): boolean {
return Boolean(this.hass?.services?.[platform]?.[serviceName]);
}
private _submitChangedSpeedMode(): void {
if (this._userEditSpeedMode && this.selectedPrinterDevice) {
const serv = "change_print_speed_mode";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed_mode: this.currentSpeedModeKey,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedFanSpeed(): void {
if (this._userEditFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedAuxFanSpeed(): void {
if (this._userEditAuxFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_aux_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentAuxFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedBoxFanSpeed(): void {
if (this._userEditBoxFanSpeed && this.selectedPrinterDevice) {
const serv = "change_print_box_fan_speed";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
speed: this.currentBoxFanSpeed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedTargetTempNozzle(): void {
if (this._userEditTargetTempNozzle && this.selectedPrinterDevice) {
const serv = "change_print_target_nozzle_temperature";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
temperature: this.currentTargetTempNozzle,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
private _submitChangedTargetTempHotbed(): void {
if (this._userEditTargetTempHotbed && this.selectedPrinterDevice) {
const serv = "change_print_target_hotbed_temperature";
if (!this._serviceAvailable(serv)) {
return;
}
this._changingSettings = true;
this.hass
.callService(platform, serv, {
config_entry: this.selectedPrinterDevice.primary_config_entry,
device_id: this.selectedPrinterDevice.id,
temperature: this.currentTargetTempHotbed,
})
.then(() => {
this._changingSettings = false;
})
.catch((_e: unknown) => {
this._changingSettings = false;
});
this._closeModal();
}
}
static get styles(): CSSResult {
return css`
${commonModalStyle}
.ac-settings-header {
font-size: 24px;
text-align: center;
font-weight: 600;
margin-bottom: 20px;
}
.ac-settings-row {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
}
.ac-disabled-feature {
display: none;
}
ha-textfield {
min-width: 150px;
width: 100%;
}
ha-control-button {
min-width: 150px;
margin: 8px 0px 0px 8px;
font-size: 14px;
}
.ac-settings-buttonrow ha-control-button {
min-width: 100%;
margin: 8px 0px 0px 8px;
font-size: 14px;
}
.ac-confirm-description {
font-size: 16px;
text-align: center;
}
.ac-confirm-buttons {
display: flex;
justify-content: center;
}
.ac-confirm-buttons ha-control-button {
margin: 20px 30px 0px 30px;
}
`;
}
}

View File

@@ -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%);
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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%;
}
`;
}
}

View File

@@ -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%;
}
`;
}
}

View File

@@ -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;
}
}
`;

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
`;
}
}

View 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"];

View File

@@ -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;
};

View File

@@ -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 };

View 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;
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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>&#11205;</span>
</a>
</div>
</div>
<div class="col w-30">
${hslChannels.map(
(c) => html`
<color-input-channel
group="hsl"
channel="${c}"
.isHsl="${this.isHsl}"
.color="${this.color}"
@color-update="${this.updateColor}"
/>
`,
)}
<div class="hsl-mode">
<a
title="Use hue / saturation / value (brightness) mode"
class="${classMap(hsvClass)}"
@click="${() => this.setHsl(false)}"
>HSV</a
><a
title="Use hue / saturation / luminosity mode"
class="${classMap(hslClass)}"
@click="${() => this.setHsl(true)}"
>HSL</a
>
</div>
</div>
<div class="w-40">
<hsl-canvas
.debounceMode="${debounceMode}"
size="${160}"
.isHsl="${this.isHsl}"
.color="${this.color}"
@color-update="${this.updateColor}"
></hsl-canvas>
<div class="ok">
<a
class="button"
.disabled=${this.buttonDisabled}
@click="${this.okColor}"
>OK
<span class="swatch">
<span style="${styleMap(swatchBg)}"></span>
<span class="checky"></span>
</span>
</a>
</div>
</div>
</div>
</div>`;
}
}
if (!window.customElements.get("color-picker")) {
window.customElements.define("color-picker", ColorPicker);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
`;

View File

@@ -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>`;

View File

@@ -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);
}

View File

@@ -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?.();
}
};

View 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;
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
}
`;

View File

@@ -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}
`;
}
}

View File

@@ -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;
});
}
};
}

View File

@@ -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;
});
}
};
}

View File

@@ -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;
});
}
};
}

View File

@@ -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;
}
`;
}
}

View File

@@ -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;
}
`;

View File

@@ -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}
`;
}
}

View File

@@ -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";
}

View File

@@ -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";
}

View 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
}
}

View File

@@ -97,6 +97,106 @@ SENSORS: tuple[KobraXSensorDescription, ...] = (
value_key="print_duration",
icon="mdi:timer-outline",
),
# Compatibility aliases used by the kobrax-lan-card (ported from anycubic card)
KobraXSensorDescription(
key="job_state",
name="Job State",
value_key="print_state",
icon="mdi:state-machine",
),
KobraXSensorDescription(
key="current_status",
name="Current Status",
value_key="print_state",
icon="mdi:information-outline",
),
KobraXSensorDescription(
key="job_progress",
name="Job Progress",
value_key="progress",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
),
KobraXSensorDescription(
key="job_time_elapsed",
name="Job Time Elapsed",
value_key="print_duration",
icon="mdi:timer-outline",
),
KobraXSensorDescription(
key="job_time_remaining",
name="Job Time Remaining",
value_key="remain_time",
icon="mdi:timer-sand",
),
KobraXSensorDescription(
key="hotbed_temperature",
name="Hotbed Temperature",
value_key="bed_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="nozzle_temperature",
name="Nozzle Temperature",
value_key="nozzle_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_hotbed_temperature",
name="Target Hotbed Temperature",
value_key="bed_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="target_nozzle_temperature",
name="Target Nozzle Temperature",
value_key="nozzle_target",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
KobraXSensorDescription(
key="job_name",
name="Job Name",
value_key="filename",
icon="mdi:file",
),
KobraXSensorDescription(
key="job_current_layer",
name="Job Current Layer",
value_key="curr_layer",
icon="mdi:layers-triple",
),
KobraXSensorDescription(
key="job_total_layers",
name="Job Total Layers",
value_key="total_layers",
icon="mdi:layers",
),
KobraXSensorDescription(
key="job_speed_mode",
name="Job Speed Mode",
value_key="print_speed_mode",
icon="mdi:speedometer",
),
KobraXSensorDescription(
key="fan_speed",
name="Fan Speed",
value_key="fan_speed",
icon="mdi:fan",
),
KobraXSensorDescription(
key="ace_spools",
name="ACE Spools",
value_key="ams_slots",
icon="mdi:palette-swatch",
),
)
@@ -110,10 +210,45 @@ class KobraXSensor(KobraXEntity, SensorEntity):
@property
def native_value(self) -> Any:
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.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.key == "current_status":
material_type = self.state_data.get("material_type") or "Filament"
return {"material_type": material_type}
if self.entity_description.key == "job_speed_mode":
mode = int(self.state_data.get("print_speed_mode") or 2)
return {
"available_modes": [
{"mode": 1, "description": "Slow"},
{"mode": 2, "description": "Normal"},
{"mode": 3, "description": "Fast"},
],
"print_speed_mode_code": mode,
}
if self.entity_description.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,
}
if self.entity_description.key == "ace_spools":
slots = self.state_data.get("ams_slots")
return {"slots": slots if isinstance(slots, list) else []}
return None
class KobraXFilamentSlotSensor(KobraXEntity, SensorEntity):
def __init__(self, coordinator, entry, slot_index: int, field: str) -> None:

View 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)

View 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