mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-14 21:22:20 +00:00
Machine properties and periodic ping (#10381)
* add machine properties * remove non working polyfill * add periodic task * add tests and docs * fix ping task * add int and float type * Update api_version.py --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `order_queryset` report helper function in [#10439](https://github.com/inventree/InvenTree/pull/10439)
|
- Added `order_queryset` report helper function in [#10439](https://github.com/inventree/InvenTree/pull/10439)
|
||||||
|
- Added much more detailed status information for machines to the API endpoint (including backend and frontend changes) in [#10381](https://github.com/inventree/InvenTree/pull/10381)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@@ -107,6 +107,7 @@ The machine type class gets instantiated for each machine on server startup and
|
|||||||
- check_setting
|
- check_setting
|
||||||
- set_status
|
- set_status
|
||||||
- set_status_text
|
- set_status_text
|
||||||
|
- set_properties
|
||||||
|
|
||||||
### Drivers
|
### Drivers
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ class MyXyzAbcDriverPlugin(MachineDriverMixin, InvenTreePlugin):
|
|||||||
- init_machine
|
- init_machine
|
||||||
- update_machine
|
- update_machine
|
||||||
- restart_machine
|
- restart_machine
|
||||||
|
- ping_machines
|
||||||
- get_machines
|
- get_machines
|
||||||
- handle_error
|
- handle_error
|
||||||
|
|
||||||
@@ -224,3 +226,24 @@ class MyXYZDriver(ABCBaseDriver):
|
|||||||
# ... do some init stuff here
|
# ... do some init stuff here
|
||||||
machine.set_status_text("Paper missing")
|
machine.set_status_text("Paper missing")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Machine Properties
|
||||||
|
|
||||||
|
Machine properties such as the device model, firmware version, and total pages printed can be displayed in the machine detail drawer to provide users with relevant device information.
|
||||||
|
|
||||||
|
To achieve this, use the `machine.set_properties` function to set the desired properties. This can be combined with a periodic task, such as `ping_machines`, to keep the information up to date.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from plugin.machine import MachineProperty
|
||||||
|
|
||||||
|
class MyXYZDriver(ABCBaseDriver):
|
||||||
|
# ...
|
||||||
|
def ping_machines(self):
|
||||||
|
for machine in self.get_machines():
|
||||||
|
# ... fetch machine info
|
||||||
|
props: list[MachineProperty] = [
|
||||||
|
{ 'key': 'Model', 'value': 'ABC' },
|
||||||
|
]
|
||||||
|
machine.set_properties(props)
|
||||||
|
|
||||||
|
```
|
||||||
|
@@ -243,6 +243,12 @@ Refer to the [return order settings](../sales/return_order.md#return-order-setti
|
|||||||
{{ globalsetting("ENABLE_PLUGINS_INTERFACE") }}
|
{{ globalsetting("ENABLE_PLUGINS_INTERFACE") }}
|
||||||
{{ globalsetting("ENABLE_PLUGINS_MAILS") }}
|
{{ globalsetting("ENABLE_PLUGINS_MAILS") }}
|
||||||
|
|
||||||
|
### Machine Settings
|
||||||
|
|
||||||
|
| Name | Description | Default | Units |
|
||||||
|
| ---- | ----------- | ------- | ----- |
|
||||||
|
{{ globalsetting("MACHINE_PING_ENABLED") }}
|
||||||
|
|
||||||
### Project Codes
|
### Project Codes
|
||||||
|
|
||||||
Refer to the [project code settings](../concepts/project_codes.md).
|
Refer to the [project code settings](../concepts/project_codes.md).
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 400
|
INVENTREE_API_VERSION = 401
|
||||||
|
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v401 -> 2025-10-04 : https://github.com/inventree/InvenTree/pull/10381
|
||||||
|
- Adds machine properties to machine API endpoints
|
||||||
|
|
||||||
v400 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10486
|
v400 -> 2025-10-05 : https://github.com/inventree/InvenTree/pull/10486
|
||||||
- Adds return datatypes for admin/config and flags entpoints
|
- Adds return datatypes for admin/config and flags entpoints
|
||||||
|
|
||||||
|
@@ -91,6 +91,13 @@ def get_cache_config(global_cache: bool) -> dict:
|
|||||||
else:
|
else:
|
||||||
redis_url = f'redis://{cache_host()}:{cache_port()}/0'
|
redis_url = f'redis://{cache_host()}:{cache_port()}/0'
|
||||||
|
|
||||||
|
keepalive_options = {
|
||||||
|
'TCP_KEEPCNT': cache_setting('keepalive_count', 5, typecast=int),
|
||||||
|
'TCP_KEEPIDLE': cache_setting('keepalive_idle', 1, typecast=int),
|
||||||
|
'TCP_KEEPINTVL': cache_setting('keepalive_interval', 1, typecast=int),
|
||||||
|
'TCP_USER_TIMEOUT': cache_setting('user_timeout', 1000, typecast=int),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'BACKEND': 'django_redis.cache.RedisCache',
|
'BACKEND': 'django_redis.cache.RedisCache',
|
||||||
'LOCATION': redis_url,
|
'LOCATION': redis_url,
|
||||||
@@ -105,18 +112,11 @@ def get_cache_config(global_cache: bool) -> dict:
|
|||||||
'tcp_keepalive', True, typecast=bool
|
'tcp_keepalive', True, typecast=bool
|
||||||
),
|
),
|
||||||
'socket_keepalive_options': {
|
'socket_keepalive_options': {
|
||||||
socket.TCP_KEEPCNT: cache_setting(
|
# Only include options which are available on this platform
|
||||||
'keepalive_count', 5, typecast=int
|
# e.g. MacOS does not have TCP_KEEPIDLE and TCP_USER_TIMEOUT
|
||||||
),
|
getattr(socket, key): value
|
||||||
socket.TCP_KEEPIDLE: cache_setting(
|
for key, value in keepalive_options.items()
|
||||||
'keepalive_idle', 1, typecast=int
|
if hasattr(socket, key)
|
||||||
),
|
|
||||||
socket.TCP_KEEPINTVL: cache_setting(
|
|
||||||
'keepalive_interval', 1, typecast=int
|
|
||||||
),
|
|
||||||
socket.TCP_USER_TIMEOUT: cache_setting(
|
|
||||||
'user_timeout', 1000, typecast=int
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -1130,4 +1130,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'default': False,
|
'default': False,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
'MACHINE_PING_ENABLED': {
|
||||||
|
'name': _('Enable Machine Ping'),
|
||||||
|
'description': _(
|
||||||
|
'Enable periodic ping task of registered machines to check their status'
|
||||||
|
),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@@ -253,7 +253,7 @@ class ApiTests(InvenTreeAPITestCase):
|
|||||||
# MachineStatus model
|
# MachineStatus model
|
||||||
machine_status = response.data['LabelPrinterStatus']
|
machine_status = response.data['LabelPrinterStatus']
|
||||||
self.assertEqual(machine_status['status_class'], 'LabelPrinterStatus')
|
self.assertEqual(machine_status['status_class'], 'LabelPrinterStatus')
|
||||||
self.assertEqual(len(machine_status['values']), 6)
|
self.assertEqual(len(machine_status['values']), 8)
|
||||||
connected = machine_status['values']['CONNECTED']
|
connected = machine_status['values']['CONNECTED']
|
||||||
self.assertEqual(connected['key'], 100)
|
self.assertEqual(connected['key'], 100)
|
||||||
self.assertEqual(connected['name'], 'CONNECTED')
|
self.assertEqual(connected['name'], 'CONNECTED')
|
||||||
|
@@ -1,4 +1,15 @@
|
|||||||
from machine.machine_type import BaseDriver, BaseMachineType, MachineStatus
|
from machine.machine_type import (
|
||||||
|
BaseDriver,
|
||||||
|
BaseMachineType,
|
||||||
|
MachineProperty,
|
||||||
|
MachineStatus,
|
||||||
|
)
|
||||||
from machine.registry import registry
|
from machine.registry import registry
|
||||||
|
|
||||||
__all__ = ['BaseDriver', 'BaseMachineType', 'MachineStatus', 'registry']
|
__all__ = [
|
||||||
|
'BaseDriver',
|
||||||
|
'BaseMachineType',
|
||||||
|
'MachineProperty',
|
||||||
|
'MachineStatus',
|
||||||
|
'registry',
|
||||||
|
]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Base machine type/base driver."""
|
"""Base machine type/base driver."""
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union
|
||||||
|
|
||||||
from generic.states import StatusCode
|
from generic.states import StatusCode
|
||||||
from InvenTree.helpers_mixin import (
|
from InvenTree.helpers_mixin import (
|
||||||
@@ -48,6 +48,24 @@ class MachineStatus(StatusCode):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MachineProperty(TypedDict, total=False):
|
||||||
|
"""Type definition for machine properties.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
key: Key of the property (required)
|
||||||
|
value: Value of the property (required)
|
||||||
|
group: Grouping of the property
|
||||||
|
type: Type of the property (one of 'str', 'bool', 'progress', 'int', 'float') default = 'str'
|
||||||
|
max_progress: Maximum value for progress type (required if type is 'progress')
|
||||||
|
"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: Union[str, bool, int, float]
|
||||||
|
group: str
|
||||||
|
type: Literal['str', 'bool', 'progress', 'int', 'float']
|
||||||
|
max_progress: Union[int, None]
|
||||||
|
|
||||||
|
|
||||||
class BaseDriver(
|
class BaseDriver(
|
||||||
ClassValidationMixin,
|
ClassValidationMixin,
|
||||||
ClassProviderMixin,
|
ClassProviderMixin,
|
||||||
@@ -117,6 +135,12 @@ class BaseDriver(
|
|||||||
machine: Machine instance
|
machine: Machine instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def ping_machines(self):
|
||||||
|
"""Ping all machines using this driver to check if they are online.
|
||||||
|
|
||||||
|
This is called periodically by a background task if the setting 'MACHINE_PING_ENABLED' is active.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_machines(self, **kwargs):
|
def get_machines(self, **kwargs):
|
||||||
"""Return all machines using this driver (By default only initialized machines).
|
"""Return all machines using this driver (By default only initialized machines).
|
||||||
|
|
||||||
@@ -139,7 +163,8 @@ class BaseDriver(
|
|||||||
Arguments:
|
Arguments:
|
||||||
error: Exception or string
|
error: Exception or string
|
||||||
"""
|
"""
|
||||||
self.set_shared_state('errors', [*self.errors, error])
|
if error not in self.errors:
|
||||||
|
self.set_shared_state('errors', [*self.errors, error])
|
||||||
|
|
||||||
# --- state getters/setters
|
# --- state getters/setters
|
||||||
@property
|
@property
|
||||||
@@ -321,7 +346,8 @@ class BaseMachineType(
|
|||||||
Arguments:
|
Arguments:
|
||||||
error: Exception or string
|
error: Exception or string
|
||||||
"""
|
"""
|
||||||
self.set_shared_state('errors', [*self.errors, error])
|
if error not in self.errors:
|
||||||
|
self.set_shared_state('errors', [*self.errors, error])
|
||||||
|
|
||||||
def reset_errors(self):
|
def reset_errors(self):
|
||||||
"""Helper function for resetting the error list for a machine."""
|
"""Helper function for resetting the error list for a machine."""
|
||||||
@@ -406,6 +432,26 @@ class BaseMachineType(
|
|||||||
"""
|
"""
|
||||||
self.set_shared_state('status_text', status_text)
|
self.set_shared_state('status_text', status_text)
|
||||||
|
|
||||||
|
def set_properties(self, properties: list[MachineProperty]):
|
||||||
|
"""Set the machine properties. This can be any arbitrary dict with model information, etc.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
properties: The new properties dict to set
|
||||||
|
"""
|
||||||
|
for p in properties:
|
||||||
|
if 'type' not in p:
|
||||||
|
p['type'] = 'str'
|
||||||
|
|
||||||
|
if 'group' not in p:
|
||||||
|
p['group'] = ''
|
||||||
|
|
||||||
|
if p['type'] == 'progress' and 'max_progress' not in p:
|
||||||
|
p['max_progress'] = 100
|
||||||
|
if 'max_progress' not in p:
|
||||||
|
p['max_progress'] = None
|
||||||
|
|
||||||
|
self.set_shared_state('properties', properties)
|
||||||
|
|
||||||
# --- state getters/setters
|
# --- state getters/setters
|
||||||
@property
|
@property
|
||||||
def initialized(self) -> bool:
|
def initialized(self) -> bool:
|
||||||
@@ -440,3 +486,13 @@ class BaseMachineType(
|
|||||||
def status_text(self) -> str:
|
def status_text(self) -> str:
|
||||||
"""Machine status text."""
|
"""Machine status text."""
|
||||||
return self.get_shared_state('status_text', '')
|
return self.get_shared_state('status_text', '')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def properties(self) -> list[MachineProperty]:
|
||||||
|
"""Return a dict of all relevant machine properties."""
|
||||||
|
return self.get_shared_state('properties', [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def properties_dict(self) -> dict[str, MachineProperty]:
|
||||||
|
"""Return a dict of all machine properties with key as dict key."""
|
||||||
|
return {prop['key']: prop for prop in self.properties}
|
||||||
|
@@ -221,16 +221,21 @@ class LabelPrinterStatus(MachineStatus):
|
|||||||
CONNECTED: The printer is connected and ready to print
|
CONNECTED: The printer is connected and ready to print
|
||||||
UNKNOWN: The printer status is unknown (e.g. there is no active connection to the printer)
|
UNKNOWN: The printer status is unknown (e.g. there is no active connection to the printer)
|
||||||
PRINTING: The printer is currently printing a label
|
PRINTING: The printer is currently printing a label
|
||||||
|
WARNING: The printer is in an unknown warning condition
|
||||||
NO_MEDIA: The printer is out of media (e.g. the label spool is empty)
|
NO_MEDIA: The printer is out of media (e.g. the label spool is empty)
|
||||||
|
PAPER_JAM: The printer has a paper jam
|
||||||
DISCONNECTED: The driver cannot establish a connection to the printer
|
DISCONNECTED: The driver cannot establish a connection to the printer
|
||||||
|
ERROR: The printer is in an unknown error condition
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CONNECTED = 100, _('Connected'), ColorEnum.success
|
CONNECTED = 100, _('Connected'), ColorEnum.success
|
||||||
UNKNOWN = 101, _('Unknown'), ColorEnum.secondary
|
UNKNOWN = 101, _('Unknown'), ColorEnum.secondary
|
||||||
PRINTING = 110, _('Printing'), ColorEnum.primary
|
PRINTING = 110, _('Printing'), ColorEnum.primary
|
||||||
|
WARNING = 200, _('Warning'), ColorEnum.warning
|
||||||
NO_MEDIA = 301, _('No media'), ColorEnum.warning
|
NO_MEDIA = 301, _('No media'), ColorEnum.warning
|
||||||
PAPER_JAM = 302, _('Paper jam'), ColorEnum.warning
|
PAPER_JAM = 302, _('Paper jam'), ColorEnum.warning
|
||||||
DISCONNECTED = 400, _('Disconnected'), ColorEnum.danger
|
DISCONNECTED = 400, _('Disconnected'), ColorEnum.danger
|
||||||
|
ERROR = 500, _('Error'), ColorEnum.danger
|
||||||
|
|
||||||
|
|
||||||
class LabelPrinterMachine(BaseMachineType):
|
class LabelPrinterMachine(BaseMachineType):
|
||||||
@@ -250,7 +255,7 @@ class LabelPrinterMachine(BaseMachineType):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MACHINE_STATUS = LabelPrinterStatus
|
MACHINE_STATUS: type[LabelPrinterStatus] = LabelPrinterStatus
|
||||||
|
|
||||||
default_machine_status = LabelPrinterStatus.UNKNOWN
|
default_machine_status = LabelPrinterStatus.UNKNOWN
|
||||||
|
|
||||||
|
@@ -125,7 +125,8 @@ class MachineRegistry(
|
|||||||
|
|
||||||
def handle_error(self, error: Union[Exception, str]):
|
def handle_error(self, error: Union[Exception, str]):
|
||||||
"""Helper function for capturing errors with the machine registry."""
|
"""Helper function for capturing errors with the machine registry."""
|
||||||
self.set_shared_state('errors', [*self.errors, error])
|
if error not in self.errors:
|
||||||
|
self.set_shared_state('errors', [*self.errors, error])
|
||||||
|
|
||||||
@machine_registry_entrypoint(check_reload=False, check_ready=False)
|
@machine_registry_entrypoint(check_reload=False, check_ready=False)
|
||||||
def initialize(self, main: bool = False):
|
def initialize(self, main: bool = False):
|
||||||
|
@@ -10,6 +10,22 @@ from machine import registry
|
|||||||
from machine.models import MachineConfig, MachineSetting
|
from machine.models import MachineConfig, MachineSetting
|
||||||
|
|
||||||
|
|
||||||
|
class MachinePropertySerializer(serializers.Serializer):
|
||||||
|
"""Serializer for a MachineProperty."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Meta for serializer."""
|
||||||
|
|
||||||
|
fields = ['key', 'value', 'group', 'type', 'max_progress']
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
key = serializers.CharField()
|
||||||
|
value = serializers.CharField()
|
||||||
|
group = serializers.CharField()
|
||||||
|
type = serializers.CharField()
|
||||||
|
max_progress = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class MachineConfigSerializer(serializers.ModelSerializer):
|
class MachineConfigSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for a MachineConfig."""
|
"""Serializer for a MachineConfig."""
|
||||||
|
|
||||||
@@ -30,9 +46,10 @@ class MachineConfigSerializer(serializers.ModelSerializer):
|
|||||||
'machine_errors',
|
'machine_errors',
|
||||||
'is_driver_available',
|
'is_driver_available',
|
||||||
'restart_required',
|
'restart_required',
|
||||||
|
'properties',
|
||||||
]
|
]
|
||||||
|
|
||||||
read_only_fields = ['machine_type', 'driver']
|
read_only_fields = ['machine_type', 'driver', 'properties']
|
||||||
|
|
||||||
initialized = serializers.SerializerMethodField('get_initialized')
|
initialized = serializers.SerializerMethodField('get_initialized')
|
||||||
status = serializers.SerializerMethodField('get_status')
|
status = serializers.SerializerMethodField('get_status')
|
||||||
@@ -41,6 +58,12 @@ class MachineConfigSerializer(serializers.ModelSerializer):
|
|||||||
machine_errors = serializers.SerializerMethodField('get_errors')
|
machine_errors = serializers.SerializerMethodField('get_errors')
|
||||||
is_driver_available = serializers.SerializerMethodField('get_is_driver_available')
|
is_driver_available = serializers.SerializerMethodField('get_is_driver_available')
|
||||||
restart_required = serializers.SerializerMethodField('get_restart_required')
|
restart_required = serializers.SerializerMethodField('get_restart_required')
|
||||||
|
properties = serializers.ListField(
|
||||||
|
child=MachinePropertySerializer(),
|
||||||
|
source='machine.properties',
|
||||||
|
read_only=True,
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
|
||||||
def get_initialized(self, obj: MachineConfig) -> bool:
|
def get_initialized(self, obj: MachineConfig) -> bool:
|
||||||
"""Serializer method for the initialized field."""
|
"""Serializer method for the initialized field."""
|
||||||
|
23
src/backend/InvenTree/machine/tasks.py
Normal file
23
src/backend/InvenTree/machine/tasks.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""Background task definitions for the 'machine' app."""
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from opentelemetry import trace
|
||||||
|
|
||||||
|
from common.settings import get_global_setting
|
||||||
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
|
from machine import registry
|
||||||
|
|
||||||
|
tracer = trace.get_tracer(__name__)
|
||||||
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
|
|
||||||
|
@tracer.start_as_current_span('ping_machines')
|
||||||
|
@scheduled_task(ScheduledTask.MINUTES, 5)
|
||||||
|
def ping_machines():
|
||||||
|
"""Periodically ping all configured machines to check if they are online."""
|
||||||
|
if not get_global_setting('MACHINE_PING_ENABLED', True):
|
||||||
|
return
|
||||||
|
|
||||||
|
for driver in registry.get_drivers():
|
||||||
|
logger.debug("Pinging machines for driver '%s'", driver.SLUG)
|
||||||
|
driver.ping_machines()
|
@@ -139,6 +139,27 @@ class TestDriverMachineInterface(TestMachineRegistryMixin, TestCase):
|
|||||||
# Test machine restart hook
|
# Test machine restart hook
|
||||||
registry.restart_machine(machine.machine)
|
registry.restart_machine(machine.machine)
|
||||||
|
|
||||||
|
# Assert properties have been set and correctly initialized with default values
|
||||||
|
self.assertDictEqual(
|
||||||
|
machine.machine.properties_dict,
|
||||||
|
{
|
||||||
|
'Model': {
|
||||||
|
'key': 'Model',
|
||||||
|
'value': 'Sample Printer 3000',
|
||||||
|
'type': 'str',
|
||||||
|
'group': '',
|
||||||
|
'max_progress': None,
|
||||||
|
},
|
||||||
|
'Battery': {
|
||||||
|
'key': 'Battery',
|
||||||
|
'value': 42,
|
||||||
|
'type': 'progress',
|
||||||
|
'group': '',
|
||||||
|
'max_progress': 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Test remove machine
|
# Test remove machine
|
||||||
self.assertEqual(len(registry.get_machines()), 2)
|
self.assertEqual(len(registry.get_machines()), 2)
|
||||||
registry.remove_machine(machine)
|
registry.remove_machine(machine)
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
from machine import BaseDriver, BaseMachineType, MachineStatus, registry
|
from machine import (
|
||||||
|
BaseDriver,
|
||||||
|
BaseMachineType,
|
||||||
|
MachineProperty,
|
||||||
|
MachineStatus,
|
||||||
|
registry,
|
||||||
|
)
|
||||||
from machine.registry import call_machine_function
|
from machine.registry import call_machine_function
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'BaseDriver',
|
'BaseDriver',
|
||||||
'BaseMachineType',
|
'BaseMachineType',
|
||||||
|
'MachineProperty',
|
||||||
'MachineStatus',
|
'MachineStatus',
|
||||||
'call_machine_function',
|
'call_machine_function',
|
||||||
'registry',
|
'registry',
|
||||||
|
@@ -31,6 +31,10 @@ class SamplePrinterDriver(LabelPrinterBaseDriver):
|
|||||||
|
|
||||||
def init_machine(self, machine: BaseMachineType) -> None:
|
def init_machine(self, machine: BaseMachineType) -> None:
|
||||||
"""Machine initialization hook."""
|
"""Machine initialization hook."""
|
||||||
|
machine.set_properties([
|
||||||
|
{'key': 'Model', 'value': 'Sample Printer 3000'},
|
||||||
|
{'key': 'Battery', 'value': 42, 'type': 'progress'},
|
||||||
|
])
|
||||||
|
|
||||||
def print_label(
|
def print_label(
|
||||||
self,
|
self,
|
||||||
|
@@ -17,6 +17,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { api } from '../../../../App';
|
import { api } from '../../../../App';
|
||||||
import { StylishText } from '../../../../components/items/StylishText';
|
import { StylishText } from '../../../../components/items/StylishText';
|
||||||
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
import { MachineListTable } from '../../../../tables/machine/MachineListTable';
|
import { MachineListTable } from '../../../../tables/machine/MachineListTable';
|
||||||
import {
|
import {
|
||||||
MachineDriverTable,
|
MachineDriverTable,
|
||||||
@@ -116,6 +117,14 @@ export default function MachineManagementPanel() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value='settings'>
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<GlobalSettingList keys={['MACHINE_PING_ENABLED']} />
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -4,14 +4,16 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
|
||||||
Code,
|
Code,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Indicator,
|
Indicator,
|
||||||
List,
|
List,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
|
Paper,
|
||||||
|
Progress,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
Text
|
Text
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
@@ -24,7 +26,7 @@ import { AddItemButton } from '@lib/components/AddItemButton';
|
|||||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { RowDeleteAction, RowEditAction } from '@lib/index';
|
import { RowDeleteAction, RowEditAction, formatDecimal } from '@lib/index';
|
||||||
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
||||||
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
import type { InvenTreeTableProps } from '@lib/types/Tables';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
@@ -70,6 +72,13 @@ interface MachineI {
|
|||||||
machine_errors: string[];
|
machine_errors: string[];
|
||||||
is_driver_available: boolean;
|
is_driver_available: boolean;
|
||||||
restart_required: boolean;
|
restart_required: boolean;
|
||||||
|
properties: {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
group: string;
|
||||||
|
type: 'str' | 'progress' | 'bool' | 'int' | 'float';
|
||||||
|
max_progress: number;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function MachineStatusIndicator({ machine }: Readonly<{ machine: MachineI }>) {
|
function MachineStatusIndicator({ machine }: Readonly<{ machine: MachineI }>) {
|
||||||
@@ -183,10 +192,11 @@ function MachineDrawer({
|
|||||||
const {
|
const {
|
||||||
data: machine,
|
data: machine,
|
||||||
refetch,
|
refetch,
|
||||||
isFetching: isMachineFetching
|
isLoading: isMachineFetching
|
||||||
} = useQuery<MachineI>({
|
} = useQuery<MachineI>({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
queryKey: ['machine-detail', machinePk],
|
queryKey: ['machine-detail', machinePk],
|
||||||
|
refetchInterval: 5 * 1000,
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
api
|
api
|
||||||
.get(apiUrl(ApiEndpoints.machine_list, machinePk))
|
.get(apiUrl(ApiEndpoints.machine_list, machinePk))
|
||||||
@@ -251,6 +261,19 @@ function MachineDrawer({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupedProperties = useMemo(() => {
|
||||||
|
if (!machine?.properties) return [];
|
||||||
|
const groups: string[] = []; // track ordered list of groups
|
||||||
|
const groupMap: { [key: string]: typeof machine.properties } = {};
|
||||||
|
for (const prop of machine.properties) {
|
||||||
|
if (!groups.includes(prop.group)) groups.push(prop.group);
|
||||||
|
if (!groupMap[prop.group]) groupMap[prop.group] = [];
|
||||||
|
groupMap[prop.group].push(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map((g) => ({ group: g, properties: groupMap[g] }));
|
||||||
|
}, [machine?.properties]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
@@ -305,17 +328,22 @@ function MachineDrawer({
|
|||||||
|
|
||||||
<Accordion
|
<Accordion
|
||||||
multiple
|
multiple
|
||||||
defaultValue={['machine-info', 'machine-settings', 'driver-settings']}
|
defaultValue={[
|
||||||
|
'machine-info',
|
||||||
|
'machine-properties',
|
||||||
|
'machine-settings',
|
||||||
|
'driver-settings'
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Accordion.Item
|
<Accordion.Item
|
||||||
key={`machine-info-${machinePk}`}
|
key={`machine-info-${machinePk}`}
|
||||||
value='machine-info'
|
value='machine-info'
|
||||||
>
|
>
|
||||||
<Accordion.Control>
|
<Accordion.Control>
|
||||||
<StylishText size='lg'>{t`Machine Information`}</StylishText>
|
<StylishText size='lg'>{t`General`}</StylishText>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Card withBorder>
|
<Paper withBorder p='md'>
|
||||||
<Stack gap='md'>
|
<Stack gap='md'>
|
||||||
<Stack pos='relative' gap='xs'>
|
<Stack pos='relative' gap='xs'>
|
||||||
<LoadingOverlay
|
<LoadingOverlay
|
||||||
@@ -363,7 +391,7 @@ function MachineDrawer({
|
|||||||
) : (
|
) : (
|
||||||
StatusRenderer({
|
StatusRenderer({
|
||||||
status: `${machine?.status || -1}`,
|
status: `${machine?.status || -1}`,
|
||||||
type: `MachineStatus__${machine?.status_model}` as any
|
type: `${machine?.status_model}` as any
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
<Text fz='sm'>{machine?.status_text}</Text>
|
<Text fz='sm'>{machine?.status_text}</Text>
|
||||||
@@ -392,7 +420,77 @@ function MachineDrawer({
|
|||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Paper>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item
|
||||||
|
key={`machine-properties-${machinePk}`}
|
||||||
|
value='machine-properties'
|
||||||
|
>
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size='lg'>{t`Properties`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Paper withBorder p='md'>
|
||||||
|
<Stack gap='sm'>
|
||||||
|
{groupedProperties.map(({ group, properties }) => (
|
||||||
|
<Stack key={group} gap={0}>
|
||||||
|
{group && (
|
||||||
|
<Text fz='sm' fw={700} mb={2}>
|
||||||
|
{group}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Table
|
||||||
|
variant='vertical'
|
||||||
|
withTableBorder
|
||||||
|
verticalSpacing={4}
|
||||||
|
>
|
||||||
|
<Table.Tbody>
|
||||||
|
{properties.map((prop) => (
|
||||||
|
<Table.Tr key={prop.key}>
|
||||||
|
<Table.Th w={250}>{prop.key}</Table.Th>
|
||||||
|
<Table.Td>
|
||||||
|
{prop.type === 'bool' ? (
|
||||||
|
<YesNoButton
|
||||||
|
value={
|
||||||
|
`${prop.value}`.toLowerCase() === 'true'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : prop.type === 'progress' ? (
|
||||||
|
<Group>
|
||||||
|
<Progress
|
||||||
|
value={
|
||||||
|
(Number.parseInt(prop.value) /
|
||||||
|
prop.max_progress) *
|
||||||
|
100
|
||||||
|
}
|
||||||
|
flex={1}
|
||||||
|
/>
|
||||||
|
<Text>
|
||||||
|
{prop.value} / {prop.max_progress}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
) : prop.type === 'int' ? (
|
||||||
|
<Text size='sm'>{prop.value}</Text>
|
||||||
|
) : prop.type === 'float' ? (
|
||||||
|
<Text size='sm'>
|
||||||
|
{formatDecimal(
|
||||||
|
Number.parseFloat(prop.value),
|
||||||
|
{ digits: 4 }
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size='sm'>{prop.value}</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
{machine?.is_driver_available && (
|
{machine?.is_driver_available && (
|
||||||
@@ -404,13 +502,13 @@ function MachineDrawer({
|
|||||||
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
|
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Card withBorder>
|
<Paper withBorder p='xs'>
|
||||||
<MachineSettingList
|
<MachineSettingList
|
||||||
machinePk={machinePk}
|
machinePk={machinePk}
|
||||||
configType='M'
|
configType='M'
|
||||||
onChange={refreshAll}
|
onChange={refreshAll}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Paper>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
)}
|
)}
|
||||||
@@ -423,13 +521,13 @@ function MachineDrawer({
|
|||||||
<StylishText size='lg'>{t`Driver Settings`}</StylishText>
|
<StylishText size='lg'>{t`Driver Settings`}</StylishText>
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<Card withBorder>
|
<Paper withBorder p='xs'>
|
||||||
<MachineSettingList
|
<MachineSettingList
|
||||||
machinePk={machinePk}
|
machinePk={machinePk}
|
||||||
configType='D'
|
configType='D'
|
||||||
onChange={refreshAll}
|
onChange={refreshAll}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Paper>
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
)}
|
)}
|
||||||
@@ -518,6 +616,15 @@ export function MachineListTable({
|
|||||||
return renderer(record);
|
return renderer(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'status_text',
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'machine_errors',
|
||||||
|
sortable: false,
|
||||||
|
render: (record) => record.machine_errors.join(', ')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[machineTypes]
|
[machineTypes]
|
||||||
|
Reference in New Issue
Block a user