187 files
This commit is contained in:
1 parent
28fa34d78a
commit
1e1b12d52b
187 files changed
+40899
-32654
No files matched your search
@@ -6,14 +6,15 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["lovelace", "http", "frontend", "persistent_notification"],
|
||||
"documentation": "https://github.com/3dg1luk43/ha_creality_ws",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"issue_tracker": "https://github.com/3dg1luk43/ha_creality_ws/issues",
|
||||
"loggers": ["custom_components.ha_creality_ws"],
|
||||
"loggers": ["websockets", "go2rtc_client"],
|
||||
"requirements": [
|
||||
"websockets>=10.4",
|
||||
"go2rtc-client>=0.1.0"
|
||||
],
|
||||
"version": "0.9.8",
|
||||
"version": "0.9.9",
|
||||
"zeroconf": [
|
||||
{ "type": "_http._tcp.local.", "name": "*creality*" },
|
||||
{ "type": "_workstation._tcp.local.", "name": "*creality*" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to Creality Printer",
|
||||
@@ -8,12 +9,14 @@
|
||||
"host": "IP Address",
|
||||
"name": "Friendly Name"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Add discovered printer",
|
||||
"description": "Add the Creality printer {name} at {host}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON.",
|
||||
"not_K": "Connection refused or not a supported Creality K-series printer.",
|
||||
"already_configured": "This printer is already configured."
|
||||
"cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
@@ -34,6 +37,7 @@
|
||||
"status_starting": "Starting",
|
||||
"status_paused": "Paused",
|
||||
"status_finishing": "Finishing",
|
||||
"status_printing": "Printing",
|
||||
"body_progress": "{progress}%",
|
||||
"body_paused": "Paused at {progress}%",
|
||||
"body_paused_unknown": "Paused",
|
||||
@@ -48,12 +52,60 @@
|
||||
"stopped": "{device} stopped at {progress}%",
|
||||
"completed_detailed": "{device} finished. {filament} consumed.",
|
||||
"error": "{device} error - {key} [{code}]",
|
||||
"filament_runout": "{device} filament ran out. Print is {state}. Change the filament.",
|
||||
"finishing_soon": "{device} will finish in {minutes} minutes."
|
||||
"filament_runout": "{device} filament ran out. Print status: {state}. Change the filament.",
|
||||
"finishing_soon": "{device} will finish in {minutes} min.",
|
||||
"cfs_info_title": "CFS information request",
|
||||
"cfs_info_failed": "Could not reach {printers}.",
|
||||
"cfs_material_failed_title": "CFS material not changed",
|
||||
"cfs_material_failed": "{printer} is not connected, so box {box} slot {slot} was not changed.",
|
||||
"diagnostic_title": "Creality diagnostic data",
|
||||
"diagnostic_collected": "Diagnostic data was collected (printers: {printers}). It is in this action's response and in the Home Assistant log."
|
||||
},
|
||||
"exceptions": {
|
||||
"unsupported_ha_version": {
|
||||
"message": "This version of the Creality WebSocket integration needs Home Assistant {minimum} or newer, but this system is running {running}. Update Home Assistant, or install integration version 0.9.7 instead."
|
||||
},
|
||||
"cfs_material_write_failed": {
|
||||
"message": "The CFS material was not changed on {printers}: the printer is not connected."
|
||||
},
|
||||
"cfs_material_needs_device": {
|
||||
"message": "Choose the printer to change. This action only writes to the printers you select, never to all of them."
|
||||
},
|
||||
"no_printer_matched": {
|
||||
"message": "None of the selected devices is a Creality printer set up in this integration."
|
||||
},
|
||||
"cfs_material_printer_busy": {
|
||||
"message": "{printer} is in the middle of a print, so its CFS material was not changed. Try again once the print is over."
|
||||
},
|
||||
"material_type_empty": {
|
||||
"message": "The material type must not be empty."
|
||||
},
|
||||
"material_not_a_number": {
|
||||
"message": "{field} must be a number, not {value}."
|
||||
},
|
||||
"material_temp_order": {
|
||||
"message": "The maximum temperature ({high}) must not be below the minimum temperature ({low})."
|
||||
},
|
||||
"material_pressure_range": {
|
||||
"message": "The pressure advance must be between 0 and 1, not {value}."
|
||||
},
|
||||
"material_colour_list": {
|
||||
"message": "The colour must be a hex value such as #ff8800, not a list of RGB values."
|
||||
},
|
||||
"material_colour_multi": {
|
||||
"message": "{value} holds several colours, and a slot can only store one."
|
||||
},
|
||||
"material_colour_invalid": {
|
||||
"message": "The colour must be six hex digits such as #ff8800, not {value}."
|
||||
},
|
||||
"printer_not_connected": {
|
||||
"message": "The printer is not connected, so the command was not sent. Check that it is switched on and reachable, then try again."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"missing_power_switch": {
|
||||
"title": "Power switch for {printer} not found",
|
||||
"description": "The power switch set for {printer}, `{entity_id}`, does not exist, so it was renamed or removed. The printer is connected to as if no power switch were set. To use one again, choose it under Settings > Devices & services > Creality WebSocket Integration > Configure > Power switch."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -173,7 +225,9 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_camera_url": "Enter a full URL including the scheme, e.g. http://192.168.1.50:8080/?action=stream or rtsp://192.168.1.50:554/stream.",
|
||||
"unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling."
|
||||
"unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling.",
|
||||
"cannot_connect": "Failed to connect to the printer at this address. Check the IP address and that the printer is switched on.",
|
||||
"host_in_use": "Another printer in this integration is already set up at this address."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -189,6 +243,7 @@
|
||||
"position_z": { "name": "Position Z" },
|
||||
"feedrate_pct": { "name": "Print Speed" },
|
||||
"flowrate_pct": { "name": "Flow Rate" },
|
||||
"real_time_speed": { "name": "Real-Time Speed" },
|
||||
"model_info": { "name": "Model" },
|
||||
"filament_status": {
|
||||
"name": "Filament Status",
|
||||
@@ -220,7 +275,10 @@
|
||||
"print_job_time": { "name": "Print Job Time" },
|
||||
"print_left_time": { "name": "Print Time Left" },
|
||||
"real_time_flow": { "name": "Real-Time Flow" },
|
||||
"current_object": { "name": "Current Object" },
|
||||
"current_object": {
|
||||
"name": "Current Object",
|
||||
"state": { "not_printing": "Not printing" }
|
||||
},
|
||||
"object_count": { "name": "Object Count" },
|
||||
"print_control": {
|
||||
"name": "Print Control",
|
||||
@@ -233,7 +291,28 @@
|
||||
"max_nozzle_temp": { "name": "Max Nozzle Temperature" },
|
||||
"max_bed_temp": { "name": "Max Bed Temperature" },
|
||||
"max_chamber_temp": { "name": "Max Chamber Temperature" },
|
||||
"active_filament_slot": { "name": "Active Filament Slot" },
|
||||
"active_filament_slot": {
|
||||
"name": "Active Filament Slot",
|
||||
"state": {
|
||||
"external": "External",
|
||||
"box_1_slot_1": "Box 1 Slot 1",
|
||||
"box_1_slot_2": "Box 1 Slot 2",
|
||||
"box_1_slot_3": "Box 1 Slot 3",
|
||||
"box_1_slot_4": "Box 1 Slot 4",
|
||||
"box_2_slot_1": "Box 2 Slot 1",
|
||||
"box_2_slot_2": "Box 2 Slot 2",
|
||||
"box_2_slot_3": "Box 2 Slot 3",
|
||||
"box_2_slot_4": "Box 2 Slot 4",
|
||||
"box_3_slot_1": "Box 3 Slot 1",
|
||||
"box_3_slot_2": "Box 3 Slot 2",
|
||||
"box_3_slot_3": "Box 3 Slot 3",
|
||||
"box_3_slot_4": "Box 3 Slot 4",
|
||||
"box_4_slot_1": "Box 4 Slot 1",
|
||||
"box_4_slot_2": "Box 4 Slot 2",
|
||||
"box_4_slot_3": "Box 4 Slot 3",
|
||||
"box_4_slot_4": "Box 4 Slot 4"
|
||||
}
|
||||
},
|
||||
"cfs_box_temp": { "name": "CFS Box {box_id} Temperature" },
|
||||
"cfs_box_humidity": { "name": "CFS Box {box_id} Humidity" },
|
||||
"cfs_slot_filament": { "name": "CFS Box {box_id} Slot {slot} Filament" },
|
||||
@@ -251,7 +330,8 @@
|
||||
"reconnect": { "name": "Reconnect" }
|
||||
},
|
||||
"number": {
|
||||
"print_tuning_pct": { "name": "Print Tuning" },
|
||||
"print_tuning_pct": { "name": "Speed Factor" },
|
||||
"flow_rate_pct": { "name": "Flow Factor" },
|
||||
"nozzle_target": { "name": "Nozzle Target" },
|
||||
"bed_target": { "name": "Bed Target" },
|
||||
"chamber_target": { "name": "Chamber Target" }
|
||||
@@ -274,11 +354,11 @@
|
||||
"services": {
|
||||
"diagnostic_dump": {
|
||||
"name": "Diagnostic Dump",
|
||||
"description": "Dump all WebSocket telemetry data from connected Creality printers to JSON format for troubleshooting and analysis.",
|
||||
"description": "Collect diagnostics for every Creality printer and return them as the response, for troubleshooting. Also written to the log.",
|
||||
"fields": {
|
||||
"include_sensitive_data": {
|
||||
"name": "Include Sensitive Data",
|
||||
"description": "Whether to include potentially sensitive data in the diagnostic output (currently unused, reserved for future use)"
|
||||
"description": "Show printer addresses and network names, notify targets and access tokens instead of hiding them. Leave this off for anything you share publicly."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to Creality Printer",
|
||||
@@ -8,12 +9,14 @@
|
||||
"host": "IP Address",
|
||||
"name": "Friendly Name"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Add discovered printer",
|
||||
"description": "Add the Creality printer {name} at {host}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON.",
|
||||
"not_K": "Connection refused or not a supported Creality K-series printer.",
|
||||
"already_configured": "This printer is already configured."
|
||||
"cannot_connect": "Failed to connect to printer. Ensure IP is correct and printer is ON."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
@@ -34,6 +37,7 @@
|
||||
"status_starting": "Starting",
|
||||
"status_paused": "Paused",
|
||||
"status_finishing": "Finishing",
|
||||
"status_printing": "Printing",
|
||||
"body_progress": "{progress}%",
|
||||
"body_paused": "Paused at {progress}%",
|
||||
"body_paused_unknown": "Paused",
|
||||
@@ -48,12 +52,60 @@
|
||||
"stopped": "{device} stopped at {progress}%",
|
||||
"completed_detailed": "{device} finished. {filament} consumed.",
|
||||
"error": "{device} error - {key} [{code}]",
|
||||
"filament_runout": "{device} filament ran out. Print is {state}. Change the filament.",
|
||||
"finishing_soon": "{device} will finish in {minutes} minutes."
|
||||
"filament_runout": "{device} filament ran out. Print status: {state}. Change the filament.",
|
||||
"finishing_soon": "{device} will finish in {minutes} min.",
|
||||
"cfs_info_title": "CFS information request",
|
||||
"cfs_info_failed": "Could not reach {printers}.",
|
||||
"cfs_material_failed_title": "CFS material not changed",
|
||||
"cfs_material_failed": "{printer} is not connected, so box {box} slot {slot} was not changed.",
|
||||
"diagnostic_title": "Creality diagnostic data",
|
||||
"diagnostic_collected": "Diagnostic data was collected (printers: {printers}). It is in this action's response and in the Home Assistant log."
|
||||
},
|
||||
"exceptions": {
|
||||
"unsupported_ha_version": {
|
||||
"message": "This version of the Creality WebSocket integration needs Home Assistant {minimum} or newer, but this system is running {running}. Update Home Assistant, or install integration version 0.9.7 instead."
|
||||
},
|
||||
"cfs_material_write_failed": {
|
||||
"message": "The CFS material was not changed on {printers}: the printer is not connected."
|
||||
},
|
||||
"cfs_material_needs_device": {
|
||||
"message": "Choose the printer to change. This action only writes to the printers you select, never to all of them."
|
||||
},
|
||||
"no_printer_matched": {
|
||||
"message": "None of the selected devices is a Creality printer set up in this integration."
|
||||
},
|
||||
"cfs_material_printer_busy": {
|
||||
"message": "{printer} is in the middle of a print, so its CFS material was not changed. Try again once the print is over."
|
||||
},
|
||||
"material_type_empty": {
|
||||
"message": "The material type must not be empty."
|
||||
},
|
||||
"material_not_a_number": {
|
||||
"message": "{field} must be a number, not {value}."
|
||||
},
|
||||
"material_temp_order": {
|
||||
"message": "The maximum temperature ({high}) must not be below the minimum temperature ({low})."
|
||||
},
|
||||
"material_pressure_range": {
|
||||
"message": "The pressure advance must be between 0 and 1, not {value}."
|
||||
},
|
||||
"material_colour_list": {
|
||||
"message": "The colour must be a hex value such as #ff8800, not a list of RGB values."
|
||||
},
|
||||
"material_colour_multi": {
|
||||
"message": "{value} holds several colours, and a slot can only store one."
|
||||
},
|
||||
"material_colour_invalid": {
|
||||
"message": "The colour must be six hex digits such as #ff8800, not {value}."
|
||||
},
|
||||
"printer_not_connected": {
|
||||
"message": "The printer is not connected, so the command was not sent. Check that it is switched on and reachable, then try again."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"missing_power_switch": {
|
||||
"title": "Power switch for {printer} not found",
|
||||
"description": "The power switch set for {printer}, `{entity_id}`, does not exist, so it was renamed or removed. The printer is connected to as if no power switch were set. To use one again, choose it under Settings > Devices & services > Creality WebSocket Integration > Configure > Power switch."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -173,7 +225,9 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_camera_url": "Enter a full URL including the scheme, e.g. http://192.168.1.50:8080/?action=stream or rtsp://192.168.1.50:554/stream.",
|
||||
"unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling."
|
||||
"unknown_placeholder": "That is not a placeholder this integration can fill. Check the list in the description above, and mind the spelling.",
|
||||
"cannot_connect": "Failed to connect to the printer at this address. Check the IP address and that the printer is switched on.",
|
||||
"host_in_use": "Another printer in this integration is already set up at this address."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -189,6 +243,7 @@
|
||||
"position_z": { "name": "Position Z" },
|
||||
"feedrate_pct": { "name": "Print Speed" },
|
||||
"flowrate_pct": { "name": "Flow Rate" },
|
||||
"real_time_speed": { "name": "Real-Time Speed" },
|
||||
"model_info": { "name": "Model" },
|
||||
"filament_status": {
|
||||
"name": "Filament Status",
|
||||
@@ -220,7 +275,10 @@
|
||||
"print_job_time": { "name": "Print Job Time" },
|
||||
"print_left_time": { "name": "Print Time Left" },
|
||||
"real_time_flow": { "name": "Real-Time Flow" },
|
||||
"current_object": { "name": "Current Object" },
|
||||
"current_object": {
|
||||
"name": "Current Object",
|
||||
"state": { "not_printing": "Not printing" }
|
||||
},
|
||||
"object_count": { "name": "Object Count" },
|
||||
"print_control": {
|
||||
"name": "Print Control",
|
||||
@@ -233,7 +291,28 @@
|
||||
"max_nozzle_temp": { "name": "Max Nozzle Temperature" },
|
||||
"max_bed_temp": { "name": "Max Bed Temperature" },
|
||||
"max_chamber_temp": { "name": "Max Chamber Temperature" },
|
||||
"active_filament_slot": { "name": "Active Filament Slot" },
|
||||
"active_filament_slot": {
|
||||
"name": "Active Filament Slot",
|
||||
"state": {
|
||||
"external": "External",
|
||||
"box_1_slot_1": "Box 1 Slot 1",
|
||||
"box_1_slot_2": "Box 1 Slot 2",
|
||||
"box_1_slot_3": "Box 1 Slot 3",
|
||||
"box_1_slot_4": "Box 1 Slot 4",
|
||||
"box_2_slot_1": "Box 2 Slot 1",
|
||||
"box_2_slot_2": "Box 2 Slot 2",
|
||||
"box_2_slot_3": "Box 2 Slot 3",
|
||||
"box_2_slot_4": "Box 2 Slot 4",
|
||||
"box_3_slot_1": "Box 3 Slot 1",
|
||||
"box_3_slot_2": "Box 3 Slot 2",
|
||||
"box_3_slot_3": "Box 3 Slot 3",
|
||||
"box_3_slot_4": "Box 3 Slot 4",
|
||||
"box_4_slot_1": "Box 4 Slot 1",
|
||||
"box_4_slot_2": "Box 4 Slot 2",
|
||||
"box_4_slot_3": "Box 4 Slot 3",
|
||||
"box_4_slot_4": "Box 4 Slot 4"
|
||||
}
|
||||
},
|
||||
"cfs_box_temp": { "name": "CFS Box {box_id} Temperature" },
|
||||
"cfs_box_humidity": { "name": "CFS Box {box_id} Humidity" },
|
||||
"cfs_slot_filament": { "name": "CFS Box {box_id} Slot {slot} Filament" },
|
||||
@@ -251,7 +330,8 @@
|
||||
"reconnect": { "name": "Reconnect" }
|
||||
},
|
||||
"number": {
|
||||
"print_tuning_pct": { "name": "Print Tuning" },
|
||||
"print_tuning_pct": { "name": "Speed Factor" },
|
||||
"flow_rate_pct": { "name": "Flow Factor" },
|
||||
"nozzle_target": { "name": "Nozzle Target" },
|
||||
"bed_target": { "name": "Bed Target" },
|
||||
"chamber_target": { "name": "Chamber Target" }
|
||||
@@ -274,11 +354,11 @@
|
||||
"services": {
|
||||
"diagnostic_dump": {
|
||||
"name": "Diagnostic Dump",
|
||||
"description": "Dump all WebSocket telemetry data from connected Creality printers to JSON format for troubleshooting and analysis.",
|
||||
"description": "Collect diagnostics for every Creality printer and return them as the response, for troubleshooting. Also written to the log.",
|
||||
"fields": {
|
||||
"include_sensitive_data": {
|
||||
"name": "Include Sensitive Data",
|
||||
"description": "Whether to include potentially sensitive data in the diagnostic output (currently unused, reserved for future use)"
|
||||
"description": "Show printer addresses and network names, notify targets and access tokens instead of hiding them. Leave this off for anything you share publicly."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Conectar a la impresora Creality",
|
||||
@@ -8,12 +9,14 @@
|
||||
"host": "Dirección IP",
|
||||
"name": "Nombre descriptivo"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Añadir impresora descubierta",
|
||||
"description": "¿Quieres añadir la impresora Creality {name} en {host}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Error al conectar con la impresora. Asegúrate de que la dirección IP sea correcta y de que la impresora esté encendida.",
|
||||
"not_K": "Conexión rechazada o no es una impresora Creality de la serie K compatible.",
|
||||
"already_configured": "Esta impresora ya está configurada."
|
||||
"cannot_connect": "Error al conectar con la impresora. Asegúrate de que la dirección IP sea correcta y de que la impresora esté encendida."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "El dispositivo ya está configurado",
|
||||
@@ -34,6 +37,7 @@
|
||||
"status_starting": "Iniciando",
|
||||
"status_paused": "En pausa",
|
||||
"status_finishing": "Finalizando",
|
||||
"status_printing": "Imprimiendo",
|
||||
"body_progress": "{progress}%",
|
||||
"body_paused": "En pausa al {progress}%",
|
||||
"body_paused_unknown": "En pausa",
|
||||
@@ -49,11 +53,59 @@
|
||||
"completed_detailed": "{device} ha terminado. Filamento consumido: {filament}.",
|
||||
"error": "Error de {device} - {key} [{code}]",
|
||||
"filament_runout": "{device} se ha quedado sin filamento. Estado de la impresión: {state}. Cambia el filamento.",
|
||||
"finishing_soon": "{device} terminará en {minutes} minutos."
|
||||
"finishing_soon": "{device} terminará en {minutes} min.",
|
||||
"cfs_info_title": "Solicitud de información del CFS",
|
||||
"cfs_info_failed": "No se ha podido contactar con {printers}.",
|
||||
"cfs_material_failed_title": "Material del CFS no cambiado",
|
||||
"cfs_material_failed": "{printer} no está conectada, así que no se ha cambiado la ranura {slot} de la caja {box}.",
|
||||
"diagnostic_title": "Datos de diagnóstico de Creality",
|
||||
"diagnostic_collected": "Se han recopilado los datos de diagnóstico (impresoras: {printers}). Están en la respuesta de esta acción y en el registro de Home Assistant."
|
||||
},
|
||||
"exceptions": {
|
||||
"unsupported_ha_version": {
|
||||
"message": "Esta versión de la integración Creality WebSocket necesita Home Assistant {minimum} o posterior, pero este sistema ejecuta la versión {running}. Actualiza Home Assistant o instala en su lugar la versión 0.9.7 de la integración."
|
||||
},
|
||||
"cfs_material_write_failed": {
|
||||
"message": "No se ha cambiado el material del CFS en {printers}: la impresora no está conectada."
|
||||
},
|
||||
"cfs_material_needs_device": {
|
||||
"message": "Elige la impresora que quieres cambiar. Esta acción solo escribe en las impresoras que selecciones, nunca en todas."
|
||||
},
|
||||
"no_printer_matched": {
|
||||
"message": "Ninguno de los dispositivos seleccionados es una impresora Creality configurada en esta integración."
|
||||
},
|
||||
"cfs_material_printer_busy": {
|
||||
"message": "{printer} está en mitad de una impresión, así que no se ha cambiado su material del CFS. Vuelve a intentarlo cuando termine la impresión."
|
||||
},
|
||||
"material_type_empty": {
|
||||
"message": "El tipo de material no puede estar vacío."
|
||||
},
|
||||
"material_not_a_number": {
|
||||
"message": "{field} debe ser un número, no {value}."
|
||||
},
|
||||
"material_temp_order": {
|
||||
"message": "La temperatura máxima ({high}) no puede ser inferior a la temperatura mínima ({low})."
|
||||
},
|
||||
"material_pressure_range": {
|
||||
"message": "El avance de presión debe estar entre 0 y 1, no {value}."
|
||||
},
|
||||
"material_colour_list": {
|
||||
"message": "El color debe ser un valor hexadecimal como #ff8800, no una lista de valores RGB."
|
||||
},
|
||||
"material_colour_multi": {
|
||||
"message": "{value} contiene varios colores, y una ranura solo puede guardar uno."
|
||||
},
|
||||
"material_colour_invalid": {
|
||||
"message": "El color debe tener seis dígitos hexadecimales, como #ff8800, no {value}."
|
||||
},
|
||||
"printer_not_connected": {
|
||||
"message": "La impresora no está conectada, así que no se ha enviado el comando. Comprueba que esté encendida y que se pueda acceder a ella, y vuelve a intentarlo."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"missing_power_switch": {
|
||||
"title": "No se encuentra el interruptor de encendido de {printer}",
|
||||
"description": "El interruptor de encendido configurado para {printer}, `{entity_id}`, no existe, así que se ha cambiado de nombre o se ha eliminado. La integración se conecta a la impresora como si no se hubiera configurado ninguno. Para volver a usar uno, elígelo en Ajustes > Dispositivos y servicios > Creality WebSocket Integration > Configurar > Interruptor de encendido."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -173,7 +225,9 @@
|
||||
},
|
||||
"error": {
|
||||
"invalid_camera_url": "Introduce una URL completa incluyendo el esquema, p. ej. http://192.168.1.50:8080/?action=stream o rtsp://192.168.1.50:554/stream.",
|
||||
"unknown_placeholder": "Ese no es un marcador de posición que esta integración pueda rellenar. Consulta la lista que aparece en la descripción de arriba y comprueba que esté bien escrito."
|
||||
"unknown_placeholder": "Ese no es un marcador de posición que esta integración pueda rellenar. Consulta la lista que aparece en la descripción de arriba y comprueba que esté bien escrito.",
|
||||
"cannot_connect": "No se ha podido conectar con la impresora en esta dirección. Comprueba la dirección IP y que la impresora esté encendida.",
|
||||
"host_in_use": "Otra impresora de esta integración ya está configurada en esta dirección."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -189,6 +243,7 @@
|
||||
"position_z": { "name": "Posición Z" },
|
||||
"feedrate_pct": { "name": "Velocidad de impresión" },
|
||||
"flowrate_pct": { "name": "Tasa de flujo" },
|
||||
"real_time_speed": { "name": "Velocidad en tiempo real" },
|
||||
"model_info": { "name": "Modelo" },
|
||||
"filament_status": {
|
||||
"name": "Estado del filamento",
|
||||
@@ -220,7 +275,10 @@
|
||||
"print_job_time": { "name": "Tiempo de trabajo de impresión" },
|
||||
"print_left_time": { "name": "Tiempo restante de impresión" },
|
||||
"real_time_flow": { "name": "Flujo en tiempo real" },
|
||||
"current_object": { "name": "Objeto actual" },
|
||||
"current_object": {
|
||||
"name": "Objeto actual",
|
||||
"state": { "not_printing": "Sin impresión" }
|
||||
},
|
||||
"object_count": { "name": "Cantidad de objetos" },
|
||||
"print_control": {
|
||||
"name": "Control de impresión",
|
||||
@@ -233,7 +291,28 @@
|
||||
"max_nozzle_temp": { "name": "Temperatura máxima de la boquilla" },
|
||||
"max_bed_temp": { "name": "Temperatura máxima de la cama" },
|
||||
"max_chamber_temp": { "name": "Temperatura máxima de la cámara" },
|
||||
"active_filament_slot": { "name": "Ranura de filamento activa" },
|
||||
"active_filament_slot": {
|
||||
"name": "Ranura de filamento activa",
|
||||
"state": {
|
||||
"external": "Externa",
|
||||
"box_1_slot_1": "Caja 1, ranura 1",
|
||||
"box_1_slot_2": "Caja 1, ranura 2",
|
||||
"box_1_slot_3": "Caja 1, ranura 3",
|
||||
"box_1_slot_4": "Caja 1, ranura 4",
|
||||
"box_2_slot_1": "Caja 2, ranura 1",
|
||||
"box_2_slot_2": "Caja 2, ranura 2",
|
||||
"box_2_slot_3": "Caja 2, ranura 3",
|
||||
"box_2_slot_4": "Caja 2, ranura 4",
|
||||
"box_3_slot_1": "Caja 3, ranura 1",
|
||||
"box_3_slot_2": "Caja 3, ranura 2",
|
||||
"box_3_slot_3": "Caja 3, ranura 3",
|
||||
"box_3_slot_4": "Caja 3, ranura 4",
|
||||
"box_4_slot_1": "Caja 4, ranura 1",
|
||||
"box_4_slot_2": "Caja 4, ranura 2",
|
||||
"box_4_slot_3": "Caja 4, ranura 3",
|
||||
"box_4_slot_4": "Caja 4, ranura 4"
|
||||
}
|
||||
},
|
||||
"cfs_box_temp": { "name": "Temperatura de la caja CFS {box_id}" },
|
||||
"cfs_box_humidity": { "name": "Humedad de la caja CFS {box_id}" },
|
||||
"cfs_slot_filament": { "name": "Filamento (caja CFS {box_id}, ranura {slot})" },
|
||||
@@ -251,7 +330,8 @@
|
||||
"reconnect": { "name": "Reconectar" }
|
||||
},
|
||||
"number": {
|
||||
"print_tuning_pct": { "name": "Ajuste de impresión" },
|
||||
"print_tuning_pct": { "name": "Factor de velocidad" },
|
||||
"flow_rate_pct": { "name": "Factor de flujo" },
|
||||
"nozzle_target": { "name": "Objetivo de la boquilla" },
|
||||
"bed_target": { "name": "Objetivo de la cama" },
|
||||
"chamber_target": { "name": "Objetivo de la cámara" }
|
||||
@@ -274,11 +354,11 @@
|
||||
"services": {
|
||||
"diagnostic_dump": {
|
||||
"name": "Volcado de diagnóstico",
|
||||
"description": "Vuelca todos los datos de telemetría WebSocket de las impresoras Creality conectadas en formato JSON para resolución de problemas y análisis.",
|
||||
"description": "Recopila los diagnósticos de todas las impresoras Creality y los devuelve como respuesta, para resolver problemas. También se escriben en el registro.",
|
||||
"fields": {
|
||||
"include_sensitive_data": {
|
||||
"name": "Incluir datos sensibles",
|
||||
"description": "Si se deben incluir datos potencialmente sensibles en la salida de diagnóstico (actualmente sin uso, reservado para uso futuro)"
|
||||
"description": "Muestra las direcciones y los nombres de red de las impresoras, los destinos de notificación y los tokens de acceso en lugar de ocultarlos. Déjalo desactivado para todo lo que compartas públicamente."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"printer_card": {
|
||||
"default_name": "3D Printer",
|
||||
"picker_name": "Creality Printer Card",
|
||||
"picker_description": "Standalone card for Creality K-Series printers",
|
||||
"status_unknown": "Unknown",
|
||||
"confirm_stop": "Are you sure you want to stop the print?",
|
||||
"confirm_power_off": "Are you sure you want to power off the printer?",
|
||||
@@ -77,6 +80,7 @@
|
||||
"label_custom_btn": "Custom Action Entity",
|
||||
"label_custom_btn_icon": "Custom Button Icon",
|
||||
"label_custom_btn_hidden": "Hide Custom Button",
|
||||
"label_hidden_buttons": "Hidden Buttons",
|
||||
"label_button_order": "Button Order (list)",
|
||||
"label_hide_box_temp": "Hide Chamber Temperature",
|
||||
"label_pause_btn_icon": "Pause Icon Override",
|
||||
@@ -104,6 +108,7 @@
|
||||
"helper_custom_btn": "Any entity to trigger (Button, Script, Switch, etc.)",
|
||||
"helper_custom_btn_icon": "Icon for the custom button",
|
||||
"helper_custom_btn_hidden": "Hide the custom button",
|
||||
"helper_hidden_buttons": "Never shown on the card, whatever the printer is doing",
|
||||
"helper_button_order": "List of buttons to show in order (pause, resume, stop, light, power, custom)",
|
||||
"helper_hide_box_temp": "Hide the chamber temperature pill even when a sensor is configured",
|
||||
"editor_error_title": "Editor Error",
|
||||
@@ -111,9 +116,14 @@
|
||||
"editor_error_prefix": "Error:"
|
||||
},
|
||||
"cfs_card": {
|
||||
"picker_name": "Creality CFS Card",
|
||||
"label_device": "Printer",
|
||||
"btn_fill_from_device": "Fill all fields from the printer",
|
||||
"status_device_filled": "Filled {filled} of {total} fields.",
|
||||
"status_device_empty": "This printer has no CFS sensors yet.",
|
||||
"picker_description": "A card to control the Creality Filament System (CFS)",
|
||||
"no_data": "No CFS data available",
|
||||
"ext_label": "EXT",
|
||||
"cfs_label": "CFS",
|
||||
"cfs_number_label": "CFS {number}",
|
||||
"label_card_title": "Card Title",
|
||||
"label_external_filament": "External Filament",
|
||||
@@ -124,7 +134,6 @@
|
||||
"label_slot_filament": "Box {box} Slot {slot} Filament",
|
||||
"label_slot_color": "Box {box} Slot {slot} Color",
|
||||
"label_slot_percent": "Box {box} Slot {slot} Remaining Percent",
|
||||
"schema_compact_view": "Compact View (Mini Mode)",
|
||||
"schema_show_type_in_mini": "Show Filament Type in Mini Mode",
|
||||
"tab_entities": "Entities",
|
||||
"tab_theme": "Theme",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"printer_card": {
|
||||
"default_name": "Impresora 3D",
|
||||
"picker_name": "Tarjeta de impresora Creality",
|
||||
"picker_description": "Tarjeta independiente para impresoras Creality de la serie K",
|
||||
"status_unknown": "Desconocido",
|
||||
"confirm_stop": "¿Seguro que deseas detener la impresión?",
|
||||
"confirm_power_off": "¿Seguro que deseas apagar la impresora?",
|
||||
@@ -77,6 +80,7 @@
|
||||
"label_custom_btn": "Entidad de acción personalizada",
|
||||
"label_custom_btn_icon": "Icono del botón personalizado",
|
||||
"label_custom_btn_hidden": "Ocultar el botón personalizado",
|
||||
"label_hidden_buttons": "Botones ocultos",
|
||||
"label_button_order": "Orden de los botones (lista)",
|
||||
"label_hide_box_temp": "Ocultar la temperatura de la cámara",
|
||||
"label_pause_btn_icon": "Icono de pausa (sustitución)",
|
||||
@@ -104,6 +108,7 @@
|
||||
"helper_custom_btn": "Cualquier entidad a activar (botón, script, interruptor, etc.)",
|
||||
"helper_custom_btn_icon": "Icono del botón personalizado",
|
||||
"helper_custom_btn_hidden": "Ocultar el botón personalizado",
|
||||
"helper_hidden_buttons": "Nunca se muestran en la tarjeta, sea cual sea el estado de la impresora",
|
||||
"helper_button_order": "Lista de botones en orden (pause, resume, stop, light, power, custom)",
|
||||
"helper_hide_box_temp": "Ocultar el indicador de temperatura de la cámara aunque haya un sensor configurado",
|
||||
"editor_error_title": "Error del editor",
|
||||
@@ -111,9 +116,14 @@
|
||||
"editor_error_prefix": "Error:"
|
||||
},
|
||||
"cfs_card": {
|
||||
"picker_name": "Tarjeta CFS de Creality",
|
||||
"picker_description": "Tarjeta para controlar el Creality Filament System (CFS)",
|
||||
"label_device": "Impresora",
|
||||
"btn_fill_from_device": "Rellenar todos los campos desde la impresora",
|
||||
"status_device_filled": "Se han rellenado {filled} de {total} campos.",
|
||||
"status_device_empty": "Esta impresora aún no tiene sensores del CFS.",
|
||||
"no_data": "No hay datos del CFS disponibles",
|
||||
"ext_label": "EXT",
|
||||
"cfs_label": "CFS",
|
||||
"cfs_number_label": "CFS {number}",
|
||||
"label_card_title": "Título de la tarjeta",
|
||||
"label_external_filament": "Filamento externo",
|
||||
@@ -124,7 +134,6 @@
|
||||
"label_slot_filament": "Filamento (caja {box}, ranura {slot})",
|
||||
"label_slot_color": "Color (caja {box}, ranura {slot})",
|
||||
"label_slot_percent": "Restante (caja {box}, ranura {slot})",
|
||||
"schema_compact_view": "Vista compacta (modo mini)",
|
||||
"schema_show_type_in_mini": "Mostrar el tipo de filamento en modo mini",
|
||||
"tab_entities": "Entidades",
|
||||
"tab_theme": "Tema",
|
||||
|
||||
@@ -6,20 +6,28 @@ const mdi = (name) => `mdi:${name}`;
|
||||
|
||||
const ASSET_URL_BASE = "/ha_creality_ws/";
|
||||
const I18N_URL_BASE = `${ASSET_URL_BASE}i18n/`;
|
||||
const _i18nData = {};
|
||||
const _i18nPromises = {};
|
||||
// One cache for both cards, on the page: each module used to fetch en.json for
|
||||
// itself, and a language with no file (a 404) was asked for again by every new
|
||||
// card. A 404 is now remembered; only a failed request is retried. `no-cache`
|
||||
// revalidates, so an updated translation is not served stale (R45).
|
||||
const _i18nShared = (globalThis.__haCrealityWsI18n = globalThis.__haCrealityWsI18n || { data: {}, promises: {} });
|
||||
const _i18nData = _i18nShared.data;
|
||||
function _loadI18n(lang) {
|
||||
if (_i18nData[lang]) return Promise.resolve(_i18nData[lang]);
|
||||
if (_i18nPromises[lang]) return _i18nPromises[lang];
|
||||
_i18nPromises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`)
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
if (lang in _i18nData) return Promise.resolve(_i18nData[lang]);
|
||||
const promises = _i18nShared.promises;
|
||||
if (promises[lang]) return promises[lang];
|
||||
promises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`, { cache: "no-cache" })
|
||||
.then((res) => {
|
||||
if (res.ok) return res.json();
|
||||
if (res.status === 404) return null;
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) _i18nData[lang] = data;
|
||||
else _i18nPromises[lang] = null;
|
||||
_i18nData[lang] = data;
|
||||
return data;
|
||||
})
|
||||
.catch(() => { _i18nPromises[lang] = null; return null; });
|
||||
return _i18nPromises[lang];
|
||||
.catch(() => { promises[lang] = null; return null; });
|
||||
return promises[lang];
|
||||
}
|
||||
function _resolveLang(hass) {
|
||||
return hass?.locale?.language || hass?.language || "en";
|
||||
@@ -33,9 +41,12 @@ function _translate(hass, section, fallbackDict, key, vars) {
|
||||
: (remoteEn && key in remoteEn) ? remoteEn[key]
|
||||
: (fallbackDict[lang]?.[key] ?? fallbackDict[short]?.[key] ?? fallbackDict["en"]?.[key] ?? key);
|
||||
if (vars) {
|
||||
Object.entries(vars).forEach(([k, v]) => {
|
||||
text = text.replace(new RegExp(`\\{${k}\\}`, "g"), v);
|
||||
});
|
||||
// One pass with a function: a replacement *string* expands `$&` and
|
||||
// friends, so a preset named "Teal $& Co" toasted as "Teal {name} Co", and
|
||||
// a value containing "{other}" was substituted again by a later key.
|
||||
text = text.replace(/\{(\w+)\}/g, (match, name) => (
|
||||
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
|
||||
));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -153,15 +164,6 @@ class ColourPresetsManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
rename(from, to) {
|
||||
const target = String(to || "").trim();
|
||||
if (!target || !(from in this.presets) || target === from) return false;
|
||||
this.presets[target] = this.presets[from];
|
||||
delete this.presets[from];
|
||||
this._persist();
|
||||
return true;
|
||||
}
|
||||
|
||||
remove(name) {
|
||||
if (!(name in this.presets)) return false;
|
||||
delete this.presets[name];
|
||||
@@ -219,9 +221,14 @@ const BUSY_PRINT_STATES = new Set([
|
||||
|
||||
const CFS_TRANSLATIONS = {
|
||||
en: {
|
||||
picker_name: "Creality CFS Card",
|
||||
label_device: "Printer",
|
||||
btn_fill_from_device: "Fill all fields from the printer",
|
||||
status_device_filled: "Filled {filled} of {total} fields.",
|
||||
status_device_empty: "This printer has no CFS sensors yet.",
|
||||
picker_description: "A card to control the Creality Filament System (CFS)",
|
||||
no_data: "No CFS data available",
|
||||
ext_label: "EXT",
|
||||
cfs_label: "CFS",
|
||||
cfs_number_label: "CFS {number}",
|
||||
// Editor
|
||||
label_card_title: "Card Title",
|
||||
@@ -290,6 +297,19 @@ class KCFSCard extends HTMLElement {
|
||||
* used to prefill it like a real value -- so changing only the material type
|
||||
* and saving wrote `#cccccc` to the spool as though the printer had said so.
|
||||
*/
|
||||
/**
|
||||
* The external spool's label: its name, plus the material type only when
|
||||
* the name does not already say it. The filament sensor's state is already
|
||||
* "Generic PLA", so appending the type read "Generic PLA PLA" (#115).
|
||||
*/
|
||||
static _nameWithType(name, type) {
|
||||
const n = String(name ?? "").trim();
|
||||
const t = String(type ?? "").trim();
|
||||
if (!t || t === "-") return n;
|
||||
const words = n.toLowerCase().split(/\s+/);
|
||||
return words.includes(t.toLowerCase()) ? n : `${n} ${t}`;
|
||||
}
|
||||
|
||||
static _parseColor(value) {
|
||||
if (isSentinel(value)) return null;
|
||||
const raw = String(value).trim();
|
||||
@@ -411,7 +431,25 @@ class KCFSCard extends HTMLElement {
|
||||
return '#f44336'; // Red (60-100%) - Critical
|
||||
}
|
||||
|
||||
static getStubConfig() {
|
||||
/**
|
||||
* What a new card starts with: the first printer that has CFS sensors, else
|
||||
* nothing. Only what differs from the defaults goes into the dashboard
|
||||
* (R44); every default used to be written there.
|
||||
*/
|
||||
static getStubConfig(hass) {
|
||||
const registry = hass?.entities || {};
|
||||
const devices = [...new Set(Object.values(registry)
|
||||
.filter((e) => e?.platform === "ha_creality_ws" && e.device_id)
|
||||
.map((e) => e.device_id))];
|
||||
for (const deviceId of devices) {
|
||||
const found = cfsEntitiesForDevice(hass, deviceId);
|
||||
if (Object.keys(found).length) return { device: deviceId, ...found };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Every option with its default value. */
|
||||
static defaultConfig() {
|
||||
const cfg = {
|
||||
name: "CFS",
|
||||
view_mode: "full",
|
||||
@@ -439,7 +477,7 @@ class KCFSCard extends HTMLElement {
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
this._cfg = { ...KCFSCard.getStubConfig(), ...KCFSCard._migrateConfig(config) };
|
||||
this._cfg = { ...KCFSCard.defaultConfig(), ...KCFSCard._migrateConfig(config) };
|
||||
if (!this._root) {
|
||||
this._root = this.attachShadow({ mode: "open" });
|
||||
}
|
||||
@@ -460,9 +498,6 @@ class KCFSCard extends HTMLElement {
|
||||
}
|
||||
|
||||
// i18n helpers -------------------------------------------------------
|
||||
_resolveLanguage() {
|
||||
return _resolveLang(this._hass);
|
||||
}
|
||||
_t(key, vars) {
|
||||
return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars);
|
||||
}
|
||||
@@ -741,6 +776,10 @@ class KCFSCard extends HTMLElement {
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.status-badge { animation: none; }
|
||||
}
|
||||
|
||||
/* === COMPACT MODE === */
|
||||
.compact-mode {
|
||||
padding: 14px;
|
||||
@@ -858,14 +897,16 @@ class KCFSCard extends HTMLElement {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.env-mini .temp {
|
||||
color: #ffb74d;
|
||||
/* The hue says warm, dry or damp; the theme's text colour carries the
|
||||
contrast. The bare #ffb74d was 1.73:1 on a light theme (R42). */
|
||||
.env-mini .temp, .env-temp {
|
||||
color: color-mix(in srgb, #ffb74d 45%, var(--primary-text-color));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.env-mini .hum {
|
||||
.env-mini .hum, .env-hum {
|
||||
color: color-mix(in srgb, var(--hum-color, #64b5f6) 45%, var(--primary-text-color));
|
||||
font-weight: 600;
|
||||
/* Cor aplicada dinamicamente via inline style */
|
||||
}
|
||||
|
||||
/* === EXTERNAL SECTION === */
|
||||
@@ -1079,7 +1120,17 @@ class KCFSCard extends HTMLElement {
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
/* Undo the <dialog> defaults: the element is the full-screen backdrop. */
|
||||
border: none;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
box-sizing: border-box;
|
||||
color: inherit;
|
||||
}
|
||||
.edit-overlay::backdrop { background: transparent; }
|
||||
.edit-dialog {
|
||||
width: min(420px, 100%);
|
||||
max-height: 85vh;
|
||||
@@ -1293,8 +1344,14 @@ class KCFSCard extends HTMLElement {
|
||||
const name = filamentObj?.state;
|
||||
const type = filamentObj?.attributes?.type;
|
||||
const selected = filamentObj?.attributes?.selected;
|
||||
const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex;
|
||||
const parsedColor = KCFSCard._parseColor(rawColor);
|
||||
// Parsed in turn, not `||`: "unknown"/"unavailable" are truthy, so a
|
||||
// colour sensor reading either hid the slot's color_hex attribute and
|
||||
// the spool went grey (R28).
|
||||
const rawColor = isSentinel(colorObj?.state)
|
||||
? filamentObj?.attributes?.color_hex
|
||||
: colorObj.state;
|
||||
const parsedColor = KCFSCard._parseColor(rawColor)
|
||||
?? KCFSCard._parseColor(filamentObj?.attributes?.color_hex);
|
||||
const color = parsedColor ?? "#cccccc";
|
||||
const percent = KCFSCard._parsePercent(percentObj);
|
||||
const percentText = fmtState(percentObj);
|
||||
@@ -1361,8 +1418,11 @@ class KCFSCard extends HTMLElement {
|
||||
const name = filamentObj?.state;
|
||||
const type = filamentObj?.attributes?.type;
|
||||
const selected = filamentObj?.attributes?.selected;
|
||||
const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex;
|
||||
const parsedColor = KCFSCard._parseColor(rawColor);
|
||||
const rawColor = isSentinel(colorObj?.state)
|
||||
? filamentObj?.attributes?.color_hex
|
||||
: colorObj.state;
|
||||
const parsedColor = KCFSCard._parseColor(rawColor)
|
||||
?? KCFSCard._parseColor(filamentObj?.attributes?.color_hex);
|
||||
const color = parsedColor ?? "#cccccc";
|
||||
const percent = KCFSCard._parsePercent(percentObj);
|
||||
const percentText = fmtState(percentObj);
|
||||
@@ -1523,7 +1583,7 @@ class KCFSCard extends HTMLElement {
|
||||
const env = [];
|
||||
if (box?.temp && box.temp !== "-") env.push(`<span class="env-temp">${esc(box.temp)}</span>`);
|
||||
if (box?.humidity && box.humidity !== "-") {
|
||||
env.push(`<span class="env-hum" style="color: ${KCFSCard._sanitizeColor(box.humidityColor)}">${esc(box.humidity)}</span>`);
|
||||
env.push(`<span class="env-hum" style="--hum-color: ${KCFSCard._sanitizeColor(box.humidityColor)}">${esc(box.humidity)}</span>`);
|
||||
}
|
||||
|
||||
return `
|
||||
@@ -1571,7 +1631,7 @@ class KCFSCard extends HTMLElement {
|
||||
|
||||
if (tempStr || humStr) {
|
||||
const tempHtml = tempStr ? `<span class="env-temp">${esc(tempStr)}</span>` : '';
|
||||
const humHtml = humStr ? `<span class="env-hum" style="color: ${KCFSCard._sanitizeColor(selectedBox.humidityColor)}">${esc(humStr)}</span>` : '';
|
||||
const humHtml = humStr ? `<span class="env-hum" style="--hum-color: ${KCFSCard._sanitizeColor(selectedBox.humidityColor)}">${esc(humStr)}</span>` : '';
|
||||
const separator = tempStr && humStr ? ' <span style="color: var(--divider-color)">•</span> ' : '';
|
||||
envInfo = `<div class="env-info">${tempHtml}${separator}${humHtml}</div>`;
|
||||
}
|
||||
@@ -1604,7 +1664,7 @@ class KCFSCard extends HTMLElement {
|
||||
const hasFilament = safeType !== "-" && safeName !== "-";
|
||||
const pct = hasFilament && external.percent !== null ? external.percent : 0;
|
||||
const percentTextDisplay = hasFilament ? (external.percentText || '-') : '-';
|
||||
const displayName = hasFilament ? `${safeName} ${safeType}` : '-';
|
||||
const displayName = hasFilament ? KCFSCard._nameWithType(safeName, safeType) : '-';
|
||||
externalSection = `
|
||||
<div class="external-section">
|
||||
<div class="external-normal" data-eid="${esc(external.entity_id)}">
|
||||
@@ -1658,7 +1718,7 @@ class KCFSCard extends HTMLElement {
|
||||
const safeName = !isSentinel(external.name) ? String(external.name).trim() : "-";
|
||||
const hasFilament = safeType !== "-" && safeName !== "-";
|
||||
const percentTextDisplay = hasFilament ? (external.percentText || '-') : '-';
|
||||
const displayName = hasFilament ? `${safeName} ${safeType}` : '-';
|
||||
const displayName = hasFilament ? KCFSCard._nameWithType(safeName, safeType) : '-';
|
||||
return `
|
||||
<div class="external-section">
|
||||
<div class="external-compact" data-eid="${esc(external.entity_id)}">
|
||||
@@ -1682,7 +1742,7 @@ class KCFSCard extends HTMLElement {
|
||||
envHtml = `
|
||||
<div class="env-mini">
|
||||
${tempStr ? `<div class="temp">${esc(tempStr)}</div>` : ''}
|
||||
${humStr ? `<div class="hum" style="color: ${KCFSCard._sanitizeColor(box.humidityColor)}">${esc(humStr)}</div>` : ''}
|
||||
${humStr ? `<div class="hum" style="--hum-color: ${KCFSCard._sanitizeColor(box.humidityColor)}">${esc(humStr)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1823,8 +1883,7 @@ class KCFSCard extends HTMLElement {
|
||||
* rather than silently resolved to one of them.
|
||||
*
|
||||
* Reads hass.entities (EntityRegistryDisplayEntry carries device_id, platform
|
||||
* and translation_key), so no WebSocket round trip and no admin permission is
|
||||
* needed -- unlike config/entity_registry/list.
|
||||
* and translation_key), so no WebSocket round trip is needed.
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async _resolveDeviceId() {
|
||||
@@ -1844,9 +1903,8 @@ class KCFSCard extends HTMLElement {
|
||||
}
|
||||
|
||||
// Anything hass.entities could not answer for is asked individually. It is
|
||||
// not a version fallback: hass.entities can be *partially* populated, and it
|
||||
// is absent entirely for a non-admin user, whose browser is not allowed
|
||||
// config/entity_registry/get either.
|
||||
// not a version fallback: hass.entities can be *partially* populated, for
|
||||
// one while the frontend is still loading it.
|
||||
//
|
||||
// Every unresolved entity is asked, not just enough to find one device.
|
||||
// Accepting the first answer resolved a card spanning two printers to
|
||||
@@ -1866,9 +1924,9 @@ class KCFSCard extends HTMLElement {
|
||||
});
|
||||
return { deviceId: entry?.device_id || null, failed: false };
|
||||
} catch (_) {
|
||||
// config/entity_registry/get is admin-only, so for a non-admin
|
||||
// dashboard user every one of these fails. Treating that as "this
|
||||
// entity has no device" is what let a two-printer card resolve to
|
||||
// A failed lookup (an entity missing from the registry, a dropped
|
||||
// connection) is not "this entity has no device". Treating it so
|
||||
// is what let a two-printer card resolve to
|
||||
// whichever printer *was* in hass.entities, and _saveMaterial then
|
||||
// sent the other printer's box and slot ids to it.
|
||||
return { deviceId: null, failed: true };
|
||||
@@ -1985,8 +2043,13 @@ class KCFSCard extends HTMLElement {
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "cfs-toast";
|
||||
// Announced by screen readers without taking focus (R42).
|
||||
toast.setAttribute("role", "status");
|
||||
toast.setAttribute("aria-live", "polite");
|
||||
toast.textContent = message;
|
||||
this._root.appendChild(toast);
|
||||
// Inside the open edit dialog when there is one: it is in the top layer,
|
||||
// and a toast on the card would be painted under it.
|
||||
(this._dialogEl || this._root).appendChild(toast);
|
||||
this._toastEl = toast;
|
||||
this._toastTimer = setTimeout(() => {
|
||||
if (toast.remove) toast.remove();
|
||||
@@ -2038,7 +2101,11 @@ class KCFSCard extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
// A native <dialog> opened with showModal() renders in the top layer. The
|
||||
// <div> overlay before it stayed inside this card's stacking context
|
||||
// (`:host { z-index: 1 }`), so a second CFS card further down the page was
|
||||
// painted over the bottom of the dialog (R46, seen in Chromium).
|
||||
const overlay = document.createElement("dialog");
|
||||
overlay.className = "edit-overlay";
|
||||
const dialog = document.createElement("div");
|
||||
dialog.className = "edit-dialog";
|
||||
@@ -2049,11 +2116,16 @@ class KCFSCard extends HTMLElement {
|
||||
dialog.setAttribute("aria-modal", "true");
|
||||
dialog.setAttribute("aria-label", this._t("dialog_edit_title"));
|
||||
dialog.tabIndex = -1;
|
||||
overlay.appendChild(dialog);
|
||||
|
||||
// Back to whatever opened the dialog afterwards, so a keyboard user is not
|
||||
// dropped at the top of the page (R42).
|
||||
const opener = this._root.activeElement || null;
|
||||
const close = () => {
|
||||
overlay.removeEventListener("keydown", onKeyDown);
|
||||
if (overlay.close && overlay.open) overlay.close();
|
||||
if (overlay.remove) overlay.remove();
|
||||
if (this._dialogEl === overlay) this._dialogEl = null;
|
||||
if (opener && opener.focus) opener.focus();
|
||||
};
|
||||
const onKeyDown = (ev) => {
|
||||
if (ev.key === "Escape" || ev.key === "Esc") {
|
||||
@@ -2061,11 +2133,38 @@ class KCFSCard extends HTMLElement {
|
||||
close();
|
||||
}
|
||||
};
|
||||
// Tab stays inside the dialog: it is modal, and focus wandering to the
|
||||
// card behind it left a keyboard user editing an invisible page (R42).
|
||||
// Sentinels rather than counting fields, because ha-form keeps its inputs
|
||||
// in its own shadow root where the dialog cannot see which one has focus.
|
||||
const sentinel = (onFocus) => {
|
||||
const el = document.createElement("span");
|
||||
el.tabIndex = 0;
|
||||
el.className = "focus-sentinel";
|
||||
el.addEventListener("focus", onFocus);
|
||||
return el;
|
||||
};
|
||||
const before = sentinel(() => {
|
||||
const buttons = Array.from(dialog.querySelectorAll("button")).filter((btn) => !btn.disabled);
|
||||
const last = buttons[buttons.length - 1];
|
||||
if (last && last.focus) last.focus();
|
||||
});
|
||||
const after = sentinel(() => dialog.focus && dialog.focus());
|
||||
overlay.appendChild(before);
|
||||
overlay.appendChild(dialog);
|
||||
overlay.appendChild(after);
|
||||
overlay.addEventListener("keydown", onKeyDown);
|
||||
// Escape on a modal <dialog> arrives as `cancel`; close it our way.
|
||||
overlay.addEventListener("cancel", (ev) => {
|
||||
ev.preventDefault?.();
|
||||
close();
|
||||
});
|
||||
overlay.addEventListener("click", (ev) => { if (ev.target === overlay) close(); });
|
||||
|
||||
dialog.appendChild(this._renderEditForm(slot, close));
|
||||
this._root.appendChild(overlay);
|
||||
this._dialogEl = overlay;
|
||||
if (overlay.showModal) overlay.showModal();
|
||||
// Focus the dialog itself rather than the first field: ha-form upgrades
|
||||
// asynchronously, so its inputs may not exist yet.
|
||||
if (dialog.focus) dialog.focus();
|
||||
@@ -2274,18 +2373,55 @@ class KCFSCard extends HTMLElement {
|
||||
swatch.title = name;
|
||||
swatch.setAttribute("aria-label", name);
|
||||
swatch.style.background = colour;
|
||||
swatch.addEventListener("click", () => apply(colour));
|
||||
let longPressed = false;
|
||||
swatch.addEventListener("click", () => {
|
||||
// The click that ends a long press is not a pick.
|
||||
if (longPressed) {
|
||||
longPressed = false;
|
||||
return;
|
||||
}
|
||||
apply(colour);
|
||||
});
|
||||
|
||||
if (this._presets.isCustom(name)) {
|
||||
swatch.classList?.add?.("custom");
|
||||
// Long-press-free management: a modifier click removes a custom preset,
|
||||
// which keeps the row compact without a second list.
|
||||
swatch.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault?.();
|
||||
const removePreset = () => {
|
||||
if (this._presets.remove(name)) {
|
||||
this._showToast(this._t("toast_preset_deleted", { name }));
|
||||
rebuild();
|
||||
}
|
||||
};
|
||||
// A right-click removes a custom preset, which keeps the row compact
|
||||
// without a second list.
|
||||
swatch.addEventListener("contextmenu", (ev) => {
|
||||
ev.preventDefault?.();
|
||||
removePreset();
|
||||
});
|
||||
// iOS Safari fires no contextmenu on a long press, so a preset could
|
||||
// not be removed there at all: a touch held for 600 ms does it, and
|
||||
// Delete does it from the keyboard (R46).
|
||||
let pressTimer = null;
|
||||
const cancelPress = () => {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
};
|
||||
swatch.addEventListener("pointerdown", (ev) => {
|
||||
if (ev.pointerType === "mouse") return;
|
||||
cancelPress();
|
||||
pressTimer = setTimeout(() => {
|
||||
pressTimer = null;
|
||||
longPressed = true;
|
||||
removePreset();
|
||||
}, 600);
|
||||
});
|
||||
for (const type of ["pointerup", "pointercancel", "pointerleave"]) {
|
||||
swatch.addEventListener(type, cancelPress);
|
||||
}
|
||||
swatch.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Delete" || ev.key === "Backspace") {
|
||||
ev.preventDefault?.();
|
||||
removePreset();
|
||||
}
|
||||
});
|
||||
}
|
||||
swatches.appendChild(swatch);
|
||||
@@ -2469,30 +2605,20 @@ class KCFSCard extends HTMLElement {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getLayoutOptions() {
|
||||
// Count configured boxes for dynamic sizing
|
||||
let boxCount = 0;
|
||||
if (this._cfg) {
|
||||
for (let box = 0; box < 4; box++) {
|
||||
const hasBox = this._cfg[`box${box}_temp`] || this._cfg[`box${box}_humidity`] ||
|
||||
[0, 1, 2, 3].some(s => this._cfg[`box${box}_slot${s}_filament`] || this._cfg[`box${box}_slot${s}_color`] || this._cfg[`box${box}_slot${s}_percent`]);
|
||||
if (hasBox) boxCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for external filament
|
||||
const hasExternal = this._cfg?.external_filament || this._cfg?.external_color || this._cfg?.external_percent;
|
||||
const externalRows = hasExternal ? 1 : 0;
|
||||
|
||||
// Add extra space when more than 2 rows
|
||||
const totalRows = boxCount + externalRows;
|
||||
const extraPadding = totalRows > 2 ? 1 : 0;
|
||||
|
||||
const minRows = this._cfg?.view_mode === "compact" ? Math.max(1, totalRows + extraPadding) : 5;
|
||||
|
||||
/**
|
||||
* Sections-view sizing: let the grid size the cell to the card.
|
||||
*
|
||||
* `getLayoutOptions` (deprecated) reserved a fixed number of rows, five for
|
||||
* the full view, and the card forces `height: auto` so its content is not
|
||||
* clipped (#71) -- together that drew the card over whatever sat below it,
|
||||
* by 176 px for a one-box CFS at phone width (R23). `rows: "auto"` makes the
|
||||
* reserved height the card's own.
|
||||
*/
|
||||
getGridOptions() {
|
||||
return {
|
||||
grid_rows: minRows,
|
||||
grid_min_rows: minRows,
|
||||
columns: 12,
|
||||
rows: "auto",
|
||||
min_columns: 6,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2521,11 +2647,76 @@ function defineOnce(tag, cls) {
|
||||
|
||||
defineOnce(CARD_TAG, KCFSCard);
|
||||
|
||||
/**
|
||||
* This integration's CFS sensors for one device, as a card config patch.
|
||||
*
|
||||
* CFS units take card positions 0-3 in the printer's box order; the external
|
||||
* spool holder is box 0 on the printer and goes to the external fields. Box and
|
||||
* slot come from the sensors' `box_id`/`slot_id` attributes, falling back to
|
||||
* the default entity id ("..._cfs_box_1_slot_2_filament", slot 1-based) for a
|
||||
* sensor with no reading yet. Without this, a CFS card meant filling up to 51
|
||||
* entity pickers by hand (R43).
|
||||
* @param {?Object} hass
|
||||
* @param {string} deviceId
|
||||
* @return {!Object<string, string>} Config key -> entity id.
|
||||
*/
|
||||
function cfsEntitiesForDevice(hass, deviceId) {
|
||||
const registry = hass?.entities || {};
|
||||
const states = hass?.states || {};
|
||||
const patch = {};
|
||||
if (!deviceId) return patch;
|
||||
const slots = [];
|
||||
const boxes = [];
|
||||
for (const [entityId, entry] of Object.entries(registry)) {
|
||||
if (!entry || entry.device_id !== deviceId) continue;
|
||||
if (entry.platform && entry.platform !== "ha_creality_ws") continue;
|
||||
const key = entry.translation_key || "";
|
||||
const attrs = states[entityId]?.attributes || {};
|
||||
let m = /^cfs_ext_(filament|color|percent)$/.exec(key);
|
||||
if (m) {
|
||||
patch[`external_${m[1]}`] = entityId;
|
||||
continue;
|
||||
}
|
||||
m = /^cfs_slot_(filament|color|percent)$/.exec(key);
|
||||
if (m) {
|
||||
let box = attrs.box_id;
|
||||
let slot = attrs.slot_id;
|
||||
if (box === undefined || box === null || slot === undefined || slot === null) {
|
||||
const id = /_cfs_box_(\d+)_slot_(\d+)_/.exec(entityId);
|
||||
if (!id) continue;
|
||||
box = Number(id[1]);
|
||||
slot = Number(id[2]) - 1;
|
||||
}
|
||||
// Box 0 is the external spool holder; an install from before R20 still
|
||||
// has "Box 0 Slot 1" sensors for it next to the External ones.
|
||||
if (Number(box) >= 1) slots.push({ box: Number(box), slot: Number(slot), kind: m[1], entityId });
|
||||
continue;
|
||||
}
|
||||
m = /^cfs_box_(temp|humidity)$/.exec(key);
|
||||
if (m) {
|
||||
let box = attrs.box_id;
|
||||
if (box === undefined || box === null) {
|
||||
const id = /_cfs_box_(\d+)_(temperature|humidity)/.exec(entityId);
|
||||
if (!id) continue;
|
||||
box = Number(id[1]);
|
||||
}
|
||||
if (Number(box) >= 1) boxes.push({ box: Number(box), kind: m[1], entityId });
|
||||
}
|
||||
}
|
||||
const order = [...new Set([...slots, ...boxes].map((e) => e.box))].sort((a, b) => a - b).slice(0, 4);
|
||||
for (const e of slots) {
|
||||
const pos = order.indexOf(e.box);
|
||||
if (pos >= 0 && e.slot >= 0 && e.slot < 4) patch[`box${pos}_slot${e.slot}_${e.kind}`] = e.entityId;
|
||||
}
|
||||
for (const e of boxes) {
|
||||
const pos = order.indexOf(e.box);
|
||||
if (pos >= 0) patch[`box${pos}_${e.kind}`] = e.entityId;
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
class KCFSCardEditor extends HTMLElement {
|
||||
// i18n helpers -------------------------------------------------------
|
||||
_resolveLanguage() {
|
||||
return _resolveLang(this._hass);
|
||||
}
|
||||
_t(key, vars) {
|
||||
return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars);
|
||||
}
|
||||
@@ -2533,42 +2724,84 @@ class KCFSCardEditor extends HTMLElement {
|
||||
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
if (this._form) this._form.hass = hass;
|
||||
_requestI18n(this, hass, () => { if (this._root) this._render(); });
|
||||
_requestI18n(this, hass, () => this._refresh(true));
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
this._cfg = { ...KCFSCard.getStubConfig(), ...KCFSCard._migrateConfig(config) };
|
||||
this._render();
|
||||
this._cfg = { ...KCFSCard.defaultConfig(), ...KCFSCard._migrateConfig(config) };
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._render();
|
||||
this._refresh();
|
||||
}
|
||||
|
||||
_render() {
|
||||
/**
|
||||
* Build once, then only update.
|
||||
*
|
||||
* Lovelace answers every config-changed with a fresh setConfig. Rebuilding
|
||||
* the editor's DOM on each one, as this did, lost the focused field after
|
||||
* every keystroke and jumped back to the first tab (R22). Data is only
|
||||
* reassigned when it really differs from what the forms already show.
|
||||
*/
|
||||
_refresh(relabel = false) {
|
||||
if (!this._cfg) return;
|
||||
if (!this._root) {
|
||||
this._root = this.attachShadow({ mode: "open" });
|
||||
}
|
||||
if (!this._form) {
|
||||
this._build();
|
||||
relabel = true;
|
||||
}
|
||||
this._form.hass = this._hass;
|
||||
this._themeForm.hass = this._hass;
|
||||
this._deviceForm.hass = this._hass;
|
||||
if (relabel) this._applyLabels();
|
||||
this._setFormData();
|
||||
}
|
||||
|
||||
_setFormData() {
|
||||
const key = JSON.stringify(this._cfg);
|
||||
if (key === this._shownKey) return;
|
||||
this._shownKey = key;
|
||||
this._form.data = this._cfg;
|
||||
this._themeForm.data = this._cfg;
|
||||
this._deviceForm.data = { device: this._cfg.device || "" };
|
||||
this._root.getElementById("refill").disabled = !this._cfg.device;
|
||||
}
|
||||
|
||||
_build() {
|
||||
const style = `
|
||||
.editor-container { padding: 16px; }
|
||||
.tabs { display: flex; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; }
|
||||
.tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab { padding: 8px 16px; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; color: inherit; font: inherit; }
|
||||
.tab:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; }
|
||||
.tab.active { border-bottom-color: var(--primary-color); color: var(--primary-color); }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
.input-helper { font-size: 0.9em; color: var(--secondary-text-color); margin-top: 4px; padding: 0 8px; }
|
||||
.device-row { display: flex; align-items: center; gap: 12px; margin: 4px 0 16px; flex-wrap: wrap; }
|
||||
.ghost-btn { background: none; border: 1px solid var(--divider-color); border-radius: 6px; padding: 6px 12px;
|
||||
color: var(--primary-color); font: inherit; cursor: pointer; }
|
||||
.ghost-btn:disabled { color: var(--disabled-text-color); cursor: default; }
|
||||
.ghost-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
||||
.status { color: var(--secondary-text-color); font-size: 0.9em; }
|
||||
`;
|
||||
|
||||
this._root.innerHTML = `
|
||||
<style>${style}</style>
|
||||
<div class="editor-container">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="entities">${this._t("tab_entities")}</div>
|
||||
<div class="tab" data-tab="theme">${this._t("tab_theme")}</div>
|
||||
<div class="tabs" role="tablist">
|
||||
<button type="button" class="tab active" role="tab" aria-selected="true" data-tab="entities" id="tab-entities"></button>
|
||||
<button type="button" class="tab" role="tab" aria-selected="false" data-tab="theme" id="tab-theme"></button>
|
||||
</div>
|
||||
<div class="tab-content active" id="entities-tab">
|
||||
<ha-form id="device-form"></ha-form>
|
||||
<div class="device-row">
|
||||
<button type="button" class="ghost-btn" id="refill"></button>
|
||||
<span class="status" id="refill-status" role="status"></span>
|
||||
</div>
|
||||
<ha-form id="form"></ha-form>
|
||||
</div>
|
||||
<div class="tab-content" id="theme-tab">
|
||||
@@ -2578,18 +2811,82 @@ class KCFSCardEditor extends HTMLElement {
|
||||
`;
|
||||
|
||||
this._setupTabs();
|
||||
this._setupDeviceForm();
|
||||
this._setupEntitiesForm();
|
||||
this._setupThemeForm();
|
||||
}
|
||||
|
||||
_setupDeviceForm() {
|
||||
this._deviceForm = this._root.getElementById("device-form");
|
||||
this._deviceForm.schema = [{
|
||||
name: "device",
|
||||
selector: { device: { filter: [{ integration: "ha_creality_ws" }] } },
|
||||
}];
|
||||
this._deviceForm.addEventListener("value-changed", (ev) => this._onDeviceChanged(ev.detail.value || {}));
|
||||
this._root.getElementById("refill").addEventListener("click", () => this._applyDeviceFill(true));
|
||||
}
|
||||
|
||||
_onDeviceChanged(value) {
|
||||
const deviceId = value.device || "";
|
||||
if (deviceId === (this._cfg.device || "")) return;
|
||||
this._cfg = { ...this._cfg, device: deviceId };
|
||||
if (deviceId) {
|
||||
// Picking a device fills what is still blank; the button replaces too.
|
||||
this._applyDeviceFill(false);
|
||||
return;
|
||||
}
|
||||
this._setRefillStatus("");
|
||||
this._setFormData();
|
||||
this._dispatchConfigChange();
|
||||
}
|
||||
|
||||
_applyDeviceFill(overwrite) {
|
||||
const found = cfsEntitiesForDevice(this._hass, this._cfg.device);
|
||||
const patch = {};
|
||||
for (const [key, entityId] of Object.entries(found)) {
|
||||
if (overwrite || !this._cfg[key]) patch[key] = entityId;
|
||||
}
|
||||
this._cfg = { ...this._cfg, ...patch };
|
||||
const total = Object.keys(found).length;
|
||||
this._setRefillStatus(total
|
||||
? this._t("status_device_filled", { filled: Object.keys(patch).length, total })
|
||||
: this._t("status_device_empty"));
|
||||
this._setFormData();
|
||||
this._dispatchConfigChange();
|
||||
}
|
||||
|
||||
_setRefillStatus(text) {
|
||||
const el = this._root?.getElementById("refill-status");
|
||||
if (el) el.textContent = text;
|
||||
}
|
||||
|
||||
/** Everything that reads a translation, so a late language load relabels. */
|
||||
_applyLabels() {
|
||||
this._root.getElementById("tab-entities").textContent = this._t("tab_entities");
|
||||
this._root.getElementById("tab-theme").textContent = this._t("tab_theme");
|
||||
this._root.getElementById("refill").textContent = this._t("btn_fill_from_device");
|
||||
this._deviceForm.computeLabel = () => this._t("label_device");
|
||||
// New function objects, so ha-form re-renders its labels.
|
||||
this._form.computeLabel = (s) => this._entityLabel(s);
|
||||
this._themeForm.schema = this._themeSchema();
|
||||
this._themeForm.computeLabel = (s) => ({
|
||||
view_mode: this._t("schema_view_mode"),
|
||||
show_type_in_mini: this._t("schema_show_type_in_mini"),
|
||||
}[s.name] || s.name);
|
||||
}
|
||||
|
||||
_setupTabs() {
|
||||
const tabs = this._root.querySelectorAll(".tab");
|
||||
const contents = this._root.querySelectorAll(".tab-content");
|
||||
tabs.forEach((tab) => {
|
||||
tab.onclick = () => {
|
||||
tabs.forEach((t) => t.classList.remove("active"));
|
||||
tabs.forEach((t) => {
|
||||
t.classList.remove("active");
|
||||
t.setAttribute("aria-selected", "false");
|
||||
});
|
||||
contents.forEach((c) => c.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
tab.setAttribute("aria-selected", "true");
|
||||
this._root.getElementById(`${tab.dataset.tab}-tab`).classList.add("active");
|
||||
};
|
||||
});
|
||||
@@ -2597,8 +2894,6 @@ class KCFSCardEditor extends HTMLElement {
|
||||
|
||||
_setupEntitiesForm() {
|
||||
this._form = this._root.getElementById("form");
|
||||
this._form.hass = this._hass;
|
||||
this._form.data = this._cfg;
|
||||
const schema = [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "external_filament", selector: { entity: { domain: "sensor" } } },
|
||||
@@ -2617,7 +2912,15 @@ class KCFSCardEditor extends HTMLElement {
|
||||
}
|
||||
|
||||
this._form.schema = schema;
|
||||
this._form.computeLabel = (s) => {
|
||||
// Assigned unconditionally: ha-form leaves computeHelper undefined until
|
||||
// someone sets it, so guarding on it meant this never ran. Harmless here
|
||||
// only because ha-form's own default is "no helper" either way.
|
||||
this._form.computeHelper = () => "";
|
||||
|
||||
this._form.addEventListener("value-changed", (ev) => this._edited(ev.detail.value));
|
||||
}
|
||||
|
||||
_entityLabel(s) {
|
||||
if (s.name === "name") return this._t("label_card_title");
|
||||
if (s.name === "external_filament") return this._t("label_external_filament");
|
||||
if (s.name === "external_color") return this._t("label_external_color");
|
||||
@@ -2642,23 +2945,22 @@ class KCFSCardEditor extends HTMLElement {
|
||||
}
|
||||
|
||||
return s.name;
|
||||
};
|
||||
// Assigned unconditionally: ha-form leaves computeHelper undefined until
|
||||
// someone sets it, so guarding on it meant this never ran. Harmless here
|
||||
// only because ha-form's own default is "no helper" either way.
|
||||
this._form.computeHelper = () => "";
|
||||
|
||||
this._form.addEventListener("value-changed", (ev) => {
|
||||
this._cfg = { ...this._cfg, ...ev.detail.value };
|
||||
this._dispatchConfigChange();
|
||||
});
|
||||
}
|
||||
|
||||
_setupThemeForm() {
|
||||
const themeForm = this._root.getElementById("theme-form");
|
||||
themeForm.hass = this._hass;
|
||||
themeForm.data = this._cfg;
|
||||
themeForm.schema = [
|
||||
this._themeForm = this._root.getElementById("theme-form");
|
||||
this._themeForm.addEventListener("value-changed", (ev) => this._edited(ev.detail.value));
|
||||
}
|
||||
|
||||
/** One edit: show it, then tell Lovelace, whose echo is then a no-op. */
|
||||
_edited(value) {
|
||||
this._cfg = { ...this._cfg, ...value };
|
||||
this._setFormData();
|
||||
this._dispatchConfigChange();
|
||||
}
|
||||
|
||||
_themeSchema() {
|
||||
return [
|
||||
{
|
||||
name: "view_mode",
|
||||
selector: {
|
||||
@@ -2674,20 +2976,15 @@ class KCFSCardEditor extends HTMLElement {
|
||||
},
|
||||
{ name: "show_type_in_mini", selector: { boolean: {} } },
|
||||
];
|
||||
themeForm.computeLabel = (s) => ({
|
||||
view_mode: this._t("schema_view_mode"),
|
||||
show_type_in_mini: this._t("schema_show_type_in_mini"),
|
||||
}[s.name] || s.name);
|
||||
|
||||
themeForm.addEventListener("value-changed", (ev) => {
|
||||
this._cfg = { ...this._cfg, ...ev.detail.value };
|
||||
this._dispatchConfigChange();
|
||||
});
|
||||
}
|
||||
|
||||
_dispatchConfigChange() {
|
||||
// Only what differs from the defaults (R44): the card merges them back.
|
||||
const defaults = KCFSCard.defaultConfig();
|
||||
const config = Object.fromEntries(Object.entries(this._cfg)
|
||||
.filter(([key, value]) => !(key in defaults) || value !== defaults[key]));
|
||||
this.dispatchEvent(new CustomEvent("config-changed", {
|
||||
detail: { config: this._cfg },
|
||||
detail: { config },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
@@ -2697,9 +2994,21 @@ class KCFSCardEditor extends HTMLElement {
|
||||
defineOnce(EDITOR_TAG, KCFSCardEditor);
|
||||
|
||||
window.customCards = window.customCards || [];
|
||||
window.customCards.push({
|
||||
type: "k-cfs-card",
|
||||
name: "Creality CFS Card",
|
||||
preview: true,
|
||||
description: "A card to control the Creality Filament System (CFS)"
|
||||
});
|
||||
// Once per page, like the element itself: a second copy of this module (two
|
||||
// resource entries with different ?v=) listed the card twice in the picker.
|
||||
if (!window.customCards.some((card) => card.type === "k-cfs-card")) {
|
||||
const pickerEntry = {
|
||||
type: "k-cfs-card",
|
||||
name: CFS_TRANSLATIONS.en.picker_name,
|
||||
preview: true,
|
||||
description: CFS_TRANSLATIONS.en.picker_description,
|
||||
};
|
||||
window.customCards.push(pickerEntry);
|
||||
// The picker reads the entry when it opens, so the page's language can be
|
||||
// applied once its strings arrive (R33).
|
||||
const pageHass = document.querySelector?.("home-assistant")?.hass;
|
||||
_requestI18n({}, pageHass, () => {
|
||||
pickerEntry.name = _translate(pageHass, "cfs_card", CFS_TRANSLATIONS, "picker_name");
|
||||
pickerEntry.description = _translate(pageHass, "cfs_card", CFS_TRANSLATIONS, "picker_description");
|
||||
});
|
||||
}
|
||||
@@ -2,20 +2,28 @@ const CARD_TAG = "k-printer-card";
|
||||
const EDITOR_TAG = "k-printer-card-editor";
|
||||
|
||||
const I18N_URL_BASE = "/ha_creality_ws/i18n/";
|
||||
const _i18nData = {};
|
||||
const _i18nPromises = {};
|
||||
// One cache for both cards, on the page: each module used to fetch en.json for
|
||||
// itself, and a language with no file (a 404) was asked for again by every new
|
||||
// card. A 404 is now remembered; only a failed request is retried. `no-cache`
|
||||
// revalidates, so an updated translation is not served stale (R45).
|
||||
const _i18nShared = (globalThis.__haCrealityWsI18n = globalThis.__haCrealityWsI18n || { data: {}, promises: {} });
|
||||
const _i18nData = _i18nShared.data;
|
||||
function _loadI18n(lang) {
|
||||
if (_i18nData[lang]) return Promise.resolve(_i18nData[lang]);
|
||||
if (_i18nPromises[lang]) return _i18nPromises[lang];
|
||||
_i18nPromises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`)
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
if (lang in _i18nData) return Promise.resolve(_i18nData[lang]);
|
||||
const promises = _i18nShared.promises;
|
||||
if (promises[lang]) return promises[lang];
|
||||
promises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`, { cache: "no-cache" })
|
||||
.then((res) => {
|
||||
if (res.ok) return res.json();
|
||||
if (res.status === 404) return null;
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) _i18nData[lang] = data;
|
||||
else _i18nPromises[lang] = null;
|
||||
_i18nData[lang] = data;
|
||||
return data;
|
||||
})
|
||||
.catch(() => { _i18nPromises[lang] = null; return null; });
|
||||
return _i18nPromises[lang];
|
||||
.catch(() => { promises[lang] = null; return null; });
|
||||
return promises[lang];
|
||||
}
|
||||
function _resolveLang(hass) {
|
||||
return hass?.locale?.language || hass?.language || "en";
|
||||
@@ -29,9 +37,12 @@ function _translate(hass, section, fallbackDict, key, vars) {
|
||||
: (remoteEn && key in remoteEn) ? remoteEn[key]
|
||||
: (fallbackDict[lang]?.[key] ?? fallbackDict[short]?.[key] ?? fallbackDict["en"]?.[key] ?? key);
|
||||
if (vars) {
|
||||
for (const [name, value] of Object.entries(vars)) {
|
||||
text = text.replace(new RegExp(`\\{${name}\\}`, "g"), value);
|
||||
}
|
||||
// One pass with a function: a replacement *string* expands `$&` and
|
||||
// friends, so a preset named "Teal $& Co" toasted as "Teal {name} Co", and
|
||||
// a value containing "{other}" was substituted again by a later key.
|
||||
text = text.replace(/\{(\w+)\}/g, (match, name) => (
|
||||
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
|
||||
));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -48,6 +59,13 @@ function _requestI18n(instance, hass, onLoaded) {
|
||||
// loop if size measurement on the fresh instance keeps producing the same delta.
|
||||
const LL_REBUILD_MIN_INTERVAL_MS = 2000;
|
||||
const _lastCardRebuildDispatch = new Map();
|
||||
// The size each card last measured, by its full name/status key. Lovelace answers
|
||||
// ll-rebuild by building a NEW element, which used to start at size 3, measure
|
||||
// the same wrapped row again and fire again once the throttle cleared: a rebuild
|
||||
// every two seconds on a narrow screen, with the reported size never sticking.
|
||||
// A fresh element reads its predecessor's size from here, so it measures no
|
||||
// change and stays put.
|
||||
const _measuredCardSize = new Map();
|
||||
|
||||
// How much wider, in px, the telemetry row has to get before the units it
|
||||
// dropped are worth retrying. Retrying at the width that rejected them would
|
||||
@@ -57,7 +75,13 @@ const TELEMETRY_COMPACT_HYSTERESIS = 8;
|
||||
|
||||
const INTEGRATION_DOMAIN = "ha_creality_ws";
|
||||
// The name a card carries until the user (or the device picker) names it.
|
||||
const DEFAULT_CARD_NAME = "3D Printer";
|
||||
// What the card picker used to write into every new card's YAML. Still read as
|
||||
// "no name", so such a card shows the translated default and is renamed from
|
||||
// its device like an unnamed one (R33).
|
||||
const LEGACY_DEFAULT_CARD_NAME = "3D Printer";
|
||||
function isUnnamed(name) {
|
||||
return !name || name === LEGACY_DEFAULT_CARD_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Card roles the printer's own device can fill, keyed by the translation_key
|
||||
@@ -249,9 +273,31 @@ function loadThemeFromStorage(cardId) {
|
||||
* @returns {string} Unique card identifier
|
||||
*/
|
||||
function generateCardId(config) {
|
||||
// Generate a unique ID based on the card configuration
|
||||
const key = `${config.name || "printer"}-${config.status || "unknown"}`;
|
||||
return btoa(key).replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
|
||||
return cardIdForKey(cardKey(config));
|
||||
}
|
||||
|
||||
/** The full identity a card's id is derived from: its name and status entity. */
|
||||
function cardKey(config) {
|
||||
return `${config.name || "printer"}-${config.status || "unknown"}`;
|
||||
}
|
||||
|
||||
function cardIdForKey(key) {
|
||||
// `btoa` takes Latin-1 only and throws on anything above U+00FF, so a card
|
||||
// named "Tiskárna č.1" (or with an en dash, CJK or an emoji in its name)
|
||||
// threw out of setConfig and became an error card, and in the editor the
|
||||
// throw landed inside the debounce, so the edit was silently never saved.
|
||||
// Latin-1 names keep the id they always had, so a theme stored under it is
|
||||
// still found; anything else is encoded as UTF-8 bytes first.
|
||||
let encoded;
|
||||
try {
|
||||
encoded = btoa(key);
|
||||
} catch (_err) {
|
||||
const bytes = new TextEncoder().encode(key);
|
||||
let binary = "";
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
encoded = btoa(binary);
|
||||
}
|
||||
return encoded.replace(/[^a-zA-Z0-9]/g, '').substring(0, 16);
|
||||
}
|
||||
|
||||
// Home Assistant rewrites a DURATION sensor's state into whichever display unit
|
||||
@@ -321,10 +367,68 @@ function computeColor(status) {
|
||||
return "var(--secondary-text-color)";
|
||||
}
|
||||
|
||||
/**
|
||||
* `config` minus what equals its default, the theme likewise (R44).
|
||||
* @param {!Object} config
|
||||
* @param {!Object} defaults
|
||||
* @return {!Object}
|
||||
*/
|
||||
function withoutDefaults(config, defaults) {
|
||||
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
const out = {};
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (key === "theme" && value && typeof value === "object") {
|
||||
const theme = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (!same(v, defaults.theme?.[k])) theme[k] = v;
|
||||
}
|
||||
if (Object.keys(theme).length) out.theme = theme;
|
||||
} else if (!(key in defaults) || !same(value, defaults[key])) {
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A theme value safe inside a <style> element, or "" when it is not (R46). */
|
||||
function cssValue(value) {
|
||||
const text = String(value ?? "");
|
||||
return /^[#\w\s(),.%+\-]*$/.test(text) ? text : "";
|
||||
}
|
||||
|
||||
/** Text safe inside a double-quoted HTML attribute. */
|
||||
function attr(value) {
|
||||
return String(value ?? "").replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// Entities the custom chip switches on and off rather than pressing (R46).
|
||||
const CUSTOM_TOGGLE_DOMAINS = ["switch", "light", "input_boolean", "fan", "cover"];
|
||||
|
||||
/** A theme colour, or `fallback` when it is unset or "auto". */
|
||||
function autoColour(value, fallback) {
|
||||
return !value || value === "auto" ? fallback : value;
|
||||
}
|
||||
|
||||
class KPrinterCard extends HTMLElement {
|
||||
static getStubConfig() {
|
||||
/**
|
||||
* What a new card starts with: the first printer's entities when there is
|
||||
* one, else nothing. Only what differs from the defaults goes into the
|
||||
* dashboard; every default used to be written there and then outlived any
|
||||
* later change to it (R44).
|
||||
*/
|
||||
static getStubConfig(hass) {
|
||||
const registry = hass?.entities || {};
|
||||
const deviceId = Object.values(registry).find((e) => e?.platform === INTEGRATION_DOMAIN && e.device_id)?.device_id;
|
||||
if (!deviceId) return {};
|
||||
const device = hass?.devices?.[deviceId];
|
||||
const name = device?.name_by_user || device?.name || "";
|
||||
return { ...(name ? { name } : {}), device: deviceId, ...entitiesForDevice(hass, deviceId) };
|
||||
}
|
||||
|
||||
/** Every option with its default value. */
|
||||
static defaultConfig() {
|
||||
return {
|
||||
name: DEFAULT_CARD_NAME,
|
||||
name: "",
|
||||
// Device the entity fields were filled from. Stored so the editor's
|
||||
// "fill from device" button has something to re-read; the card itself
|
||||
// resolves nothing from it, so a config written by hand needs no device.
|
||||
@@ -349,7 +453,9 @@ class KPrinterCard extends HTMLElement {
|
||||
resume_icon: "#fff",
|
||||
stop_icon: "#fff",
|
||||
light_icon_on: "#000",
|
||||
light_icon_off: "#000",
|
||||
// "auto": the theme's text colour. A fixed black was 2.26:1 against the
|
||||
// off-state grey on a dark theme (R42).
|
||||
light_icon_off: "auto",
|
||||
// Status icon and progress circle
|
||||
status_icon: "auto", // auto, or specific color
|
||||
progress_ring: "auto", // auto, or specific color
|
||||
@@ -363,18 +469,20 @@ class KPrinterCard extends HTMLElement {
|
||||
// Off-state colours for the custom button, used when it drives a
|
||||
// toggleable entity (switch/light/input_boolean).
|
||||
custom_off_bg: "rgba(150,150,150,.35)",
|
||||
custom_icon_off: "#000",
|
||||
custom_icon_off: "auto",
|
||||
// Power button. The chip CSS has always read these, but nothing ever
|
||||
// set them, so the button was the one chip the theme could not reach.
|
||||
power_on_bg: "rgba(76, 175, 80, .90)",
|
||||
power_off_bg: "rgba(150,150,150,.35)",
|
||||
power_icon_on: "#fff",
|
||||
power_icon_off: "#000",
|
||||
power_icon_off: "auto",
|
||||
},
|
||||
// Config for custom button
|
||||
custom_btn: "",
|
||||
custom_btn_icon: "", // Moved to theme editor but stored here
|
||||
custom_btn_hidden: false,
|
||||
// Buttons never shown, whatever the printer is doing (#37)
|
||||
hidden_buttons: [],
|
||||
// Order of buttons
|
||||
button_order: ['pause', 'resume', 'stop', 'light', 'power', 'custom'],
|
||||
// Icon overrides
|
||||
@@ -431,14 +539,21 @@ class KPrinterCard extends HTMLElement {
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
const defaultConfig = KPrinterCard.getStubConfig();
|
||||
const defaultConfig = KPrinterCard.defaultConfig();
|
||||
const migrated = KPrinterCard._migrateConfig(config);
|
||||
this._cfg = { ...defaultConfig, ...migrated };
|
||||
// A dashboard now holds only the colours that differ from the defaults.
|
||||
this._cfg.theme = { ...defaultConfig.theme, ...(migrated.theme || {}) };
|
||||
// Init optimistic state overrides map
|
||||
if (!this._optimisticStates) this._optimisticStates = {};
|
||||
|
||||
// Generate card ID for theme persistence
|
||||
this._cardId = generateCardId(this._cfg);
|
||||
this._sizeKey = cardKey(this._cfg);
|
||||
this._cardId = cardIdForKey(this._sizeKey);
|
||||
// A rebuilt element starts from the size its predecessor measured.
|
||||
this._cardSize = _measuredCardSize.get(this._sizeKey);
|
||||
// A new config can name new entities: the next hass must update.
|
||||
this._seenStates = null;
|
||||
|
||||
// Load saved theme if no theme is provided in config
|
||||
if (!config?.theme) {
|
||||
@@ -461,23 +576,9 @@ class KPrinterCard extends HTMLElement {
|
||||
|
||||
// Always re-render when config changes to apply new theme
|
||||
this._render();
|
||||
|
||||
// Apply theme after render to ensure DOM is ready
|
||||
this._applyTheme();
|
||||
}
|
||||
_applyTheme() {
|
||||
if (!this._root || !this._cfg.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-render with updated CSS to apply theme changes
|
||||
this._render();
|
||||
}
|
||||
|
||||
// i18n helpers -------------------------------------------------------
|
||||
_resolveLanguage() {
|
||||
return _resolveLang(this._hass);
|
||||
}
|
||||
_t(key) {
|
||||
return _translate(this._hass, "printer_card", CARD_TRANSLATIONS, key);
|
||||
}
|
||||
@@ -486,28 +587,72 @@ class KPrinterCard extends HTMLElement {
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
_requestI18n(this, hass, () => { if (this._root) this._update(); });
|
||||
if (this._root) {
|
||||
// Apply theme first, then update
|
||||
this._applyTheme();
|
||||
this._update();
|
||||
// Schedule a follow-up update shortly after initial attach to absorb entity states once Home Assistant populates them
|
||||
clearTimeout(this._initialUpdateTimer);
|
||||
this._initialUpdateTimer = setTimeout(() => {
|
||||
try { this._update(); } catch (_) { }
|
||||
}, 150);
|
||||
// Home Assistant assigns a new hass for every state change in the whole
|
||||
// instance. This used to rebuild the shadow DOM each time (the theme only
|
||||
// changes in setConfig, which renders anyway), re-parsing the stylesheet,
|
||||
// recreating every icon and dropping keyboard focus, then update twice
|
||||
// more. Now only a change to something this card shows updates it.
|
||||
if (this._root && this._relevantChange(hass)) this._update();
|
||||
}
|
||||
|
||||
/** Whether `hass` changed anything `_update` reads, recording what it saw. */
|
||||
_relevantChange(hass) {
|
||||
const ids = this._watchedEntityIds();
|
||||
const prev = this._seenStates;
|
||||
const next = {};
|
||||
let changed = !prev;
|
||||
for (const id of ids) {
|
||||
const st = hass?.states?.[id];
|
||||
next[id] = st;
|
||||
if (prev && prev[id] !== st) changed = true;
|
||||
}
|
||||
// The language picks the card's strings; the formatter, the units.
|
||||
const lang = _resolveLang(hass);
|
||||
const fmt = typeof hass?.formatEntityState;
|
||||
if (lang !== this._seenLang || fmt !== this._seenFormatter) changed = true;
|
||||
this._seenLang = lang;
|
||||
this._seenFormatter = fmt;
|
||||
this._seenStates = next;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every entity id the config names, plus the switch/light twin that
|
||||
* `_resolveEntityId` may substitute for it. Cached per config object.
|
||||
*/
|
||||
_watchedEntityIds() {
|
||||
if (this._watchedFor === this._cfg && this._watched) return this._watched;
|
||||
const ids = new Set();
|
||||
for (const value of Object.values(this._cfg || {})) {
|
||||
if (typeof value !== "string" || !/^[a-z0-9_]+\.[a-z0-9_]+$/.test(value)) continue;
|
||||
ids.add(value);
|
||||
const objectId = value.split(".")[1];
|
||||
ids.add(`switch.${objectId}`);
|
||||
ids.add(`light.${objectId}`);
|
||||
}
|
||||
this._watched = ids;
|
||||
this._watchedFor = this._cfg;
|
||||
return ids;
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return this._cardSize ?? _measuredCardSize.get(this._sizeKey) ?? 3;
|
||||
}
|
||||
getCardSize() { return this._cardSize || 3; }
|
||||
|
||||
_render() {
|
||||
if (!this._root) return;
|
||||
|
||||
// Ensure theme is always properly initialized
|
||||
const defaultConfig = KPrinterCard.getStubConfig();
|
||||
const defaultConfig = KPrinterCard.defaultConfig();
|
||||
this._cfg.theme = { ...defaultConfig.theme, ...(this._cfg.theme || {}) };
|
||||
|
||||
// Apply theme variables to CSS custom properties
|
||||
const theme = this._cfg.theme;
|
||||
// Apply theme variables to CSS custom properties. Each value lands inside a
|
||||
// <style> element, so anything that is not plainly a colour is dropped
|
||||
// (and its default used): a value with `;`, `}` or `</style>` could
|
||||
// otherwise rewrite the card's CSS or break out into markup (R46).
|
||||
const theme = Object.fromEntries(
|
||||
Object.entries(this._cfg.theme).map(([key, value]) => [key, cssValue(value)]),
|
||||
);
|
||||
|
||||
// Theme CSS custom properties - embedded directly in CSS
|
||||
const themeCSS = `
|
||||
@@ -521,7 +666,7 @@ class KPrinterCard extends HTMLElement {
|
||||
--resume-icon: ${theme.resume_icon || '#fff'};
|
||||
--stop-icon: ${theme.stop_icon || '#fff'};
|
||||
--light-icon-on: ${theme.light_icon_on || '#000'};
|
||||
--light-icon-off: ${theme.light_icon_off || '#000'};
|
||||
--light-icon-off: ${autoColour(theme.light_icon_off, 'var(--primary-text-color)')};
|
||||
--status-bg: ${theme.status_bg === 'auto' ? 'radial-gradient(var(--card-background-color) 62%, transparent 0)' : (theme.status_bg || 'radial-gradient(var(--card-background-color) 62%, transparent 0)')};
|
||||
--telemetry-icon: ${theme.telemetry_icon === 'auto' ? 'var(--secondary-text-color)' : (theme.telemetry_icon || 'var(--secondary-text-color)')};
|
||||
--telemetry-text: ${theme.telemetry_text === 'auto' ? 'var(--primary-text-color)' : (theme.telemetry_text || 'var(--primary-text-color)')};
|
||||
@@ -529,11 +674,11 @@ class KPrinterCard extends HTMLElement {
|
||||
--custom-on-bg: ${theme.custom_on_bg || theme.custom_bg || 'rgba(33, 150, 243, .90)'};
|
||||
--custom-off-bg: ${theme.custom_off_bg || 'rgba(150,150,150,.35)'};
|
||||
--custom-icon: ${theme.custom_icon || '#fff'};
|
||||
--custom-icon-off: ${theme.custom_icon_off || '#000'};
|
||||
--custom-icon-off: ${autoColour(theme.custom_icon_off, 'var(--primary-text-color)')};
|
||||
--power-on-bg: ${theme.power_on_bg || 'rgba(76, 175, 80, .90)'};
|
||||
--power-off-bg: ${theme.power_off_bg || 'rgba(150,150,150,.35)'};
|
||||
--power-icon-on: ${theme.power_icon_on || '#fff'};
|
||||
--power-icon-off: ${theme.power_icon_off || '#000'};
|
||||
--power-icon-off: ${autoColour(theme.power_icon_off, 'var(--primary-text-color)')};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -595,6 +740,11 @@ class KPrinterCard extends HTMLElement {
|
||||
color:var(--chip-fg, var(--primary-text-color));
|
||||
cursor:pointer; user-select:none; border:none; outline:none;
|
||||
}
|
||||
/* The outline is removed for mouse clicks only: a keyboard user has to
|
||||
see where focus is (R42). */
|
||||
.chip:focus-visible, .title.click:focus-visible {
|
||||
outline: 2px solid var(--primary-color); outline-offset: 2px;
|
||||
}
|
||||
.chip[hidden]{ display:none !important; }
|
||||
.chip:active { transform: translateY(1px); }
|
||||
.chip.danger { --chip-bg: var(--stop-bg, rgba(244, 67, 54, .95)); --chip-fg: var(--stop-icon, #fff); }
|
||||
@@ -737,7 +887,8 @@ class KPrinterCard extends HTMLElement {
|
||||
// the stronger warning as much as a running print does.
|
||||
const printing = ["printing", "paused", "processing"].includes(st);
|
||||
const msg = printing ? this._t("confirm_power_off_printing") : this._t("confirm_power_off");
|
||||
if (!confirm(msg)) return;
|
||||
this._confirmedAction(msg, "homeassistant.turn_off", eid);
|
||||
return;
|
||||
}
|
||||
this._toggleEntity(eid);
|
||||
} else if (id === "light") {
|
||||
@@ -748,18 +899,23 @@ class KPrinterCard extends HTMLElement {
|
||||
} else if (id === "resume") {
|
||||
this._pressButtonEntity(this._cfg.resume_btn);
|
||||
} else if (id === "stop") {
|
||||
if (confirm(this._t("confirm_stop"))) {
|
||||
this._pressButtonEntity(this._cfg.stop_btn);
|
||||
}
|
||||
const eid = this._cfg.stop_btn;
|
||||
const domain = (eid || "").split(".")[0];
|
||||
const service = ["button", "input_button"].includes(domain) ? `${domain}.press` : "homeassistant.turn_on";
|
||||
this._confirmedAction(this._t("confirm_stop"), service, eid);
|
||||
} else if (id === "custom") {
|
||||
// Custom button can be a button (press), script (turn_on/run), switch (toggle), automation (trigger), etc.
|
||||
// For simplicity, treat as toggle if switch/light/input_boolean, else press/turn_on
|
||||
// What a tap means for each kind of entity: on/off things toggle (the
|
||||
// chip already shows fans and covers as on/off, but only ever turned
|
||||
// them on), an automation runs rather than being enabled, and buttons,
|
||||
// scripts and scenes are pressed or turned on (R46).
|
||||
const eid = this._cfg.custom_btn;
|
||||
const domain = eid ? (eid.split(".")[0] || "").toLowerCase() : "";
|
||||
if (["switch", "light", "input_boolean"].includes(domain)) {
|
||||
if (CUSTOM_TOGGLE_DOMAINS.includes(domain)) {
|
||||
this._toggleEntity(eid);
|
||||
} else if (domain === "automation") {
|
||||
this._hass?.callService("automation", "trigger", { entity_id: eid });
|
||||
} else {
|
||||
this._pressButtonEntity(eid); // Fallback to press (works for button domain, or generic turn_on if mapped)
|
||||
this._pressButtonEntity(eid);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -783,7 +939,6 @@ class KPrinterCard extends HTMLElement {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
clearTimeout(this._initialUpdateTimer);
|
||||
if (this._telemetryResizeObserver) {
|
||||
if (this._telemetryObservedNode) {
|
||||
this._telemetryResizeObserver.unobserve(this._telemetryObservedNode);
|
||||
@@ -900,25 +1055,45 @@ class KPrinterCard extends HTMLElement {
|
||||
const currentSize = this._cardSize ?? 3;
|
||||
if (nextSize === currentSize) return;
|
||||
|
||||
const cardKey = this._cardId || CARD_TAG;
|
||||
const throttleKey = this._sizeKey || CARD_TAG;
|
||||
const now = Date.now();
|
||||
const lastDispatch = _lastCardRebuildDispatch.get(cardKey) || 0;
|
||||
const lastDispatch = _lastCardRebuildDispatch.get(throttleKey) || 0;
|
||||
// Defer the _cardSize update until the throttle clears: otherwise a throttled
|
||||
// call would record the new size locally without telling Lovelace, and the
|
||||
// next measurement would short-circuit on the equality check above -- leaving
|
||||
// the rebuild permanently suppressed.
|
||||
if (now - lastDispatch < LL_REBUILD_MIN_INTERVAL_MS) return;
|
||||
_lastCardRebuildDispatch.set(cardKey, now);
|
||||
_lastCardRebuildDispatch.set(throttleKey, now);
|
||||
this._cardSize = nextSize;
|
||||
_measuredCardSize.set(this._sizeKey, nextSize);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("ll-rebuild", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
// Re-implement _pressButtonEntity to be smarter about non-button domains if needed,
|
||||
// but existing implementation calls button.press.
|
||||
// For custom buttons (e.g. scripts), we might want to default to homeassistant.turn_on if button.press fails is overkill,
|
||||
// but let's keep it simple: if it's a script/automation, button.press might not work.
|
||||
// Let's refine _pressButtonEntity to handle more types or create a generic helper.
|
||||
/**
|
||||
* Run `service` on `entityId` behind Home Assistant's own confirmation
|
||||
* dialog. window.confirm() is switched off in some kiosk browsers and
|
||||
* webviews, where it answers "no" without asking, so Stop and power-off
|
||||
* silently did nothing there (R46).
|
||||
*/
|
||||
_confirmedAction(text, service, entityId) {
|
||||
if (!entityId) return;
|
||||
this.dispatchEvent(new CustomEvent("hass-action", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
action: "tap",
|
||||
config: {
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: service,
|
||||
target: { entity_id: entityId },
|
||||
confirmation: { text },
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async _pressButtonEntity(eid) {
|
||||
if (!this._hass || !eid) return;
|
||||
@@ -926,7 +1101,7 @@ class KPrinterCard extends HTMLElement {
|
||||
if (domain === "button" || domain === "input_button") {
|
||||
await this._hass.callService(domain, "press", { entity_id: eid });
|
||||
} else {
|
||||
// Fallback for scripts, automations, scenes which act like "press" via turn_on
|
||||
// Scripts and scenes run through turn_on.
|
||||
await this._hass.callService("homeassistant", "turn_on", { entity_id: eid });
|
||||
}
|
||||
}
|
||||
@@ -1014,7 +1189,7 @@ class KPrinterCard extends HTMLElement {
|
||||
};
|
||||
const fmtWithUnit = (eid) => fmtState(gObj(eid));
|
||||
|
||||
const name = this._cfg.name || DEFAULT_CARD_NAME;
|
||||
const name = isUnnamed(this._cfg.name) ? this._t("default_name") : this._cfg.name;
|
||||
const status = g(this._cfg.status) ?? "unknown";
|
||||
const pct = clamp(Number.isFinite(gNum(this._cfg.progress)) ? gNum(this._cfg.progress) : 0, 0, 100);
|
||||
const timeLeft = durationToSeconds(gObj(this._cfg.time_left));
|
||||
@@ -1139,11 +1314,12 @@ class KPrinterCard extends HTMLElement {
|
||||
if (!uniqueOrder.includes(k) && buttons[k]) uniqueOrder.push(k);
|
||||
});
|
||||
|
||||
const hiddenByConfig = new Set(Array.isArray(this._cfg.hidden_buttons) ? this._cfg.hidden_buttons : []);
|
||||
let chipsHtml = "";
|
||||
uniqueOrder.forEach(key => {
|
||||
const btn = buttons[key];
|
||||
if (btn && !btn.hidden) {
|
||||
chipsHtml += `<button class="chip ${btn.class}" id="${key}" title="${btn.title}"><ha-icon icon="${btn.icon}"></ha-icon></button>`;
|
||||
if (btn && !btn.hidden && !hiddenByConfig.has(key)) {
|
||||
chipsHtml += `<button class="chip ${btn.class}" id="${key}" title="${attr(btn.title)}" aria-label="${attr(btn.title)}"><ha-icon icon="${attr(btn.icon)}"></ha-icon></button>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1189,6 +1365,9 @@ class KPrinterCard extends HTMLElement {
|
||||
}
|
||||
const CARD_TRANSLATIONS = {
|
||||
en: {
|
||||
default_name: "3D Printer",
|
||||
picker_name: "Creality Printer Card",
|
||||
picker_description: "Standalone card for Creality K-Series printers",
|
||||
status_unknown: "Unknown",
|
||||
confirm_stop: "Are you sure you want to stop the print?",
|
||||
confirm_power_off: "Are you sure you want to power off the printer?",
|
||||
@@ -1266,6 +1445,7 @@ const CARD_TRANSLATIONS = {
|
||||
label_custom_btn: "Custom Action Entity",
|
||||
label_custom_btn_icon: "Custom Button Icon",
|
||||
label_custom_btn_hidden: "Hide Custom Button",
|
||||
label_hidden_buttons: "Hidden Buttons",
|
||||
label_button_order: "Button Order (list)",
|
||||
label_hide_box_temp: "Hide Chamber Temperature",
|
||||
label_pause_btn_icon: "Pause Icon Override",
|
||||
@@ -1293,6 +1473,7 @@ const CARD_TRANSLATIONS = {
|
||||
helper_custom_btn: "Any entity to trigger (Button, Script, Switch, etc.)",
|
||||
helper_custom_btn_icon: "Icon for the custom button",
|
||||
helper_custom_btn_hidden: "Hide the custom button",
|
||||
helper_hidden_buttons: "Never shown on the card, whatever the printer is doing",
|
||||
helper_button_order: "List of buttons to show in order (pause, resume, stop, light, power, custom)",
|
||||
helper_hide_box_temp: "Hide the chamber temperature pill even when a sensor is configured",
|
||||
editor_error_title: "Editor Error",
|
||||
@@ -1323,7 +1504,6 @@ function defineOnce(tag, cls) {
|
||||
}
|
||||
}
|
||||
|
||||
defineOnce(CARD_TAG, KPrinterCard);
|
||||
|
||||
/**
|
||||
* Colour controls in the theme tab, grouped the way they are rendered.
|
||||
@@ -1355,15 +1535,15 @@ const THEME_COLOR_GROUPS = [
|
||||
{ key: "light_on_bg", alpha: true },
|
||||
{ key: "light_icon_on" },
|
||||
{ key: "light_off_bg", alpha: true },
|
||||
{ key: "light_icon_off" },
|
||||
{ key: "light_icon_off", auto: true, seed: "#000000" },
|
||||
{ key: "power_on_bg", alpha: true },
|
||||
{ key: "power_icon_on" },
|
||||
{ key: "power_off_bg", alpha: true },
|
||||
{ key: "power_icon_off" },
|
||||
{ key: "power_icon_off", auto: true, seed: "#000000" },
|
||||
{ key: "custom_bg", alpha: true },
|
||||
{ key: "custom_icon" },
|
||||
{ key: "custom_off_bg", alpha: true },
|
||||
{ key: "custom_icon_off" },
|
||||
{ key: "custom_icon_off", auto: true, seed: "#000000" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1405,7 +1585,7 @@ const AUTO_SUFFIX = "_auto";
|
||||
|
||||
/** Top-level config keys the theme tab owns, and so the reset button clears. */
|
||||
const LAYOUT_RESET_KEYS = [
|
||||
"button_order", "custom_btn_hidden", "hide_box_temp",
|
||||
"button_order", "custom_btn_hidden", "hidden_buttons", "hide_box_temp",
|
||||
"pause_btn_icon", "resume_btn_icon", "stop_btn_icon",
|
||||
"light_btn_icon", "power_btn_icon", "custom_btn_icon",
|
||||
];
|
||||
@@ -1419,7 +1599,8 @@ const EDITOR_STYLE = `
|
||||
.editor-container { padding: 16px; max-width: 1200px; margin: 0 auto; }
|
||||
.editor-title { margin: 0 0 16px 0; font-size: 18px; color: var(--primary-text-color); }
|
||||
.tabs { display: flex; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; }
|
||||
.tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab { padding: 8px 16px; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; color: inherit; font: inherit; }
|
||||
.tab:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; }
|
||||
.tab.active { border-bottom-color: var(--primary-color); color: var(--primary-color); }
|
||||
.tab-content { display: none; }
|
||||
.tab-content.active { display: block; }
|
||||
@@ -1493,10 +1674,23 @@ function entitiesSchema() {
|
||||
];
|
||||
}
|
||||
|
||||
function layoutSchema() {
|
||||
/** The chips `hidden_buttons` can name, in their default order. */
|
||||
const HIDEABLE_BUTTONS = ["pause", "resume", "stop", "light", "power", "custom"];
|
||||
|
||||
function layoutSchema(t = (key) => key) {
|
||||
return [
|
||||
{ name: "button_order", selector: { text: {} } },
|
||||
{ name: "custom_btn_hidden", selector: { boolean: {} } },
|
||||
{
|
||||
name: "hidden_buttons",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: HIDEABLE_BUTTONS.map((key) => ({ value: key, label: t(`chip_${key}`) })),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "hide_box_temp", selector: { boolean: {} } },
|
||||
{ name: "pause_btn_icon", selector: { icon: {} } },
|
||||
{ name: "resume_btn_icon", selector: { icon: {} } },
|
||||
@@ -1602,9 +1796,6 @@ function colorData(cfg, group) {
|
||||
/* Visual editor: entity wiring on one tab, appearance on the other. */
|
||||
class KPrinterCardEditor extends HTMLElement {
|
||||
// i18n helpers -------------------------------------------------------
|
||||
_resolveLanguage() {
|
||||
return _resolveLang(this._hass);
|
||||
}
|
||||
_t(key, vars) {
|
||||
return _translate(this._hass, "printer_card", CARD_TRANSLATIONS, key, vars);
|
||||
}
|
||||
@@ -1622,7 +1813,7 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
}
|
||||
|
||||
setConfig(config) {
|
||||
const defaults = KPrinterCard.getStubConfig();
|
||||
const defaults = KPrinterCard.defaultConfig();
|
||||
this._cfg = { ...defaults, ...KPrinterCard._migrateConfig(config) };
|
||||
this._cfg.theme = { ...defaults.theme, ...(this._cfg.theme || {}) };
|
||||
this._refresh();
|
||||
@@ -1665,9 +1856,9 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
<style>${EDITOR_STYLE}</style>
|
||||
<div class="editor-container">
|
||||
<h2 class="editor-title" id="editor-title"></h2>
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="entities" id="tab-entities"></div>
|
||||
<div class="tab" data-tab="theme" id="tab-theme"></div>
|
||||
<div class="tabs" role="tablist">
|
||||
<button type="button" class="tab active" role="tab" aria-selected="true" data-tab="entities" id="tab-entities"></button>
|
||||
<button type="button" class="tab" role="tab" aria-selected="false" data-tab="theme" id="tab-theme"></button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="entities-tab">
|
||||
@@ -1751,6 +1942,7 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
_selectTab(name) {
|
||||
for (const tab of this._root.querySelectorAll(".tab")) {
|
||||
tab.classList.toggle("active", tab.dataset.tab === name);
|
||||
tab.setAttribute("aria-selected", String(tab.dataset.tab === name));
|
||||
}
|
||||
for (const content of this._root.querySelectorAll(".tab-content")) {
|
||||
content.classList.toggle("active", content.id === `${name}-tab`);
|
||||
@@ -1852,7 +2044,7 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
|
||||
this._applyForm("device-form", deviceSchema(), { device: this._cfg.device || "" });
|
||||
this._applyForm("entities-form", entitiesSchema(), entitiesData(this._cfg));
|
||||
this._applyForm("layout-form", layoutSchema(), layoutData(this._cfg));
|
||||
this._applyForm("layout-form", layoutSchema((key) => this._t(key)), layoutData(this._cfg));
|
||||
|
||||
THEME_COLOR_GROUPS.forEach((group, index) => {
|
||||
this._root.getElementById(`group-color-${index}`).textContent = this._t(group.title);
|
||||
@@ -1903,7 +2095,7 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
}
|
||||
|
||||
_onColorChanged(group, value) {
|
||||
const defaults = KPrinterCard.getStubConfig().theme;
|
||||
const defaults = KPrinterCard.defaultConfig().theme;
|
||||
const theme = { ...this._cfg.theme };
|
||||
const wasAuto = new Map(group.fields.map((field) => [field.key, isAutoColor(this._cfg, field)]));
|
||||
for (const field of group.fields) {
|
||||
@@ -1976,9 +2168,9 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
}
|
||||
const device = this._hass?.devices?.[deviceId];
|
||||
const deviceName = device?.name_by_user || device?.name || "";
|
||||
// Every card starts life named "3D Printer", so that counts as unset --
|
||||
// Cards used to start life named "3D Printer", so that counts as unset --
|
||||
// otherwise the field the user most expects to be filled never would be.
|
||||
const nameUnset = !this._cfg.name || this._cfg.name === DEFAULT_CARD_NAME;
|
||||
const nameUnset = isUnnamed(this._cfg.name);
|
||||
if (deviceName && (overwrite || nameUnset)) patch.name = deviceName;
|
||||
return patch;
|
||||
}
|
||||
@@ -2008,7 +2200,7 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
// Reset ----------------------------------------------------------------
|
||||
|
||||
_resetTheme() {
|
||||
const defaults = KPrinterCard.getStubConfig();
|
||||
const defaults = KPrinterCard.defaultConfig();
|
||||
const cfg = { ...this._cfg, theme: { ...defaults.theme } };
|
||||
for (const key of LAYOUT_RESET_KEYS) cfg[key] = defaults[key];
|
||||
|
||||
@@ -2042,20 +2234,36 @@ class KPrinterCardEditor extends HTMLElement {
|
||||
// localStorage write. The card saves the same thing again from setConfig.
|
||||
saveThemeToStorage(generateCardId(this._cfg), this._cfg.theme);
|
||||
this.dispatchEvent(new CustomEvent("config-changed", {
|
||||
detail: { config: this._cfg },
|
||||
detail: { config: withoutDefaults(this._cfg, KPrinterCard.defaultConfig()) },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Defined last, once every module-level constant the classes read exists:
|
||||
// defining the card upgrades elements already in the page on the spot, and
|
||||
// THEME_COLOR_FIELDS (used by _migrateTheme) was declared after it (R46).
|
||||
defineOnce(CARD_TAG, KPrinterCard);
|
||||
defineOnce(EDITOR_TAG, KPrinterCardEditor);
|
||||
|
||||
try {
|
||||
window.customCards = window.customCards || [];
|
||||
window.customCards.push({
|
||||
type: CARD_TAG,
|
||||
name: "Creality Printer Card",
|
||||
description: "Standalone card for Creality K-Series printers",
|
||||
preview: true,
|
||||
});
|
||||
// Once per page, like the element itself: a second copy of this module (two
|
||||
// resource entries with different ?v=) listed the card twice in the picker.
|
||||
if (!window.customCards.some((card) => card.type === CARD_TAG)) {
|
||||
const pickerEntry = {
|
||||
type: CARD_TAG,
|
||||
name: CARD_TRANSLATIONS.en.picker_name,
|
||||
description: CARD_TRANSLATIONS.en.picker_description,
|
||||
preview: true,
|
||||
};
|
||||
window.customCards.push(pickerEntry);
|
||||
// The picker reads the entry when it opens, so the page's language can be
|
||||
// applied once its strings arrive (R33).
|
||||
const pageHass = document.querySelector?.("home-assistant")?.hass;
|
||||
_requestI18n({}, pageHass, () => {
|
||||
pickerEntry.name = _translate(pageHass, "printer_card", CARD_TRANSLATIONS, "picker_name");
|
||||
pickerEntry.description = _translate(pageHass, "printer_card", CARD_TRANSLATIONS, "picker_description");
|
||||
});
|
||||
}
|
||||
} catch (_) { }
|
||||
Reference in new issue
Block a user