105 lines
4.7 KiB
Python
105 lines
4.7 KiB
Python
"""Dashboard default model: display-PC-IP -> display role (+ business unit).
|
|
|
|
A single "display" PC image resolves what it should show from its own IP: the
|
|
role (shopfloor dashboard, lobby slideshow, or 3D-parts kiosk) and, for the
|
|
dashboard role, which business unit. Powers the visitor-location + display-role
|
|
lookups the kiosks call at launch.
|
|
"""
|
|
|
|
from shopdb.extensions import db
|
|
from .base import BaseModel
|
|
|
|
# Display role -> the frontend kiosk path it maps to. THE one place a role is
|
|
# named, and the reason the names look like this: these are the literal values a
|
|
# person types into C:\Enrollment\display-type.txt on the kiosk itself, which the
|
|
# GE-Enforce dispatcher reads to pick a target. Two vocabularies used to exist -
|
|
# core said 'partskiosk' where the kiosk said '3DPrintRoom' - so a display could
|
|
# report a role core could not store and core could store a role no kiosk would
|
|
# ever match. The machine's own file wins, because that is what a person edits.
|
|
DISPLAY_ROLE_PATHS = {
|
|
'Dashboard': '/shopfloor',
|
|
'Lobby': '/tv',
|
|
'3DPrintRoom': '/parts-kiosk',
|
|
}
|
|
DISPLAY_ROLES = tuple(DISPLAY_ROLE_PATHS.keys())
|
|
|
|
# Retired spellings -> canonical. 'partskiosk' is what core called the 3D print
|
|
# room before the vocabularies were merged; rows and API callers still carry it.
|
|
LEGACY_DISPLAY_ROLES = {
|
|
'partskiosk': '3DPrintRoom',
|
|
}
|
|
|
|
|
|
def normalize_display_role(value):
|
|
"""Canonical role for any spelling, or None if it names no role.
|
|
|
|
Case-insensitive on purpose. The authoritative value is one line of a text
|
|
file edited by hand on the kiosk, so 'lobby', 'Lobby' and 'LOBBY' all arrive
|
|
in practice; the dispatcher already matches its own map case-insensitively
|
|
(-ieq) and this keeps the server agreeing with it. Retired spellings map
|
|
forward. Returns None rather than guessing, so an unrecognised value can be
|
|
stored verbatim and SEEN rather than silently rewritten to a wrong role.
|
|
"""
|
|
text = (value or '').strip()
|
|
if not text:
|
|
return None
|
|
lowered = text.lower()
|
|
for role in DISPLAY_ROLES:
|
|
if role.lower() == lowered:
|
|
return role
|
|
return LEGACY_DISPLAY_ROLES.get(lowered)
|
|
|
|
# GE device naming: a PC's DNS name is 'F' + its BIOS serial under the device
|
|
# domain, e.g. FABC1234.device.geaerospace.net. The domain is a setting so other # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
|
# sites can point elsewhere; the collector already reports the serial, so the
|
|
# server derives the stable FQDN without the kiosk having to report it.
|
|
DEFAULT_DISPLAY_FQDN_DOMAIN = 'device.geaerospace.net' # ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
|
|
|
|
|
|
def derive_display_fqdn(serialnumber):
|
|
"""FQDN for a display PC from its BIOS serial: F<serial>.<domain> (lower).
|
|
|
|
Returns None when there is no serial. Domain from the display_fqdn_domain
|
|
setting, falling back to DEFAULT_DISPLAY_FQDN_DOMAIN.
|
|
"""
|
|
serial = (serialnumber or '').strip()
|
|
if not serial:
|
|
return None
|
|
from .setting import Setting
|
|
domain = (Setting.get('display_fqdn_domain', DEFAULT_DISPLAY_FQDN_DOMAIN)
|
|
or DEFAULT_DISPLAY_FQDN_DOMAIN).strip().strip('.')
|
|
return f'F{serial}.{domain}'.lower()
|
|
|
|
|
|
class DashboardDefault(BaseModel):
|
|
"""Maps a display PC (by FQDN, or IP) to its display role (+ location)."""
|
|
__tablename__ = 'dashboarddefaults'
|
|
|
|
dashboarddefaultid = db.Column(db.Integer, primary_key=True)
|
|
# A mapping is keyed by FQDN (stable, DHCP-proof) and/or IP. At least one is
|
|
# required (enforced in the API). FQDN is preferred; IP is the fallback for a
|
|
# manual entry or a PC not yet reporting a serial.
|
|
# 191 = the utf8mb4-safe unique-index length (191*4 < 767) so the index does
|
|
# not depend on innodb_large_prefix; FQDNs (F<serial>.<domain>) fit easily.
|
|
fqdn = db.Column(db.String(191), unique=True, nullable=True, index=True)
|
|
ipaddress = db.Column(db.String(50), unique=True, nullable=True)
|
|
# Which display the mapping drives. Only the Dashboard role uses businessunitid.
|
|
displayrole = db.Column(db.String(20), nullable=False, default='Dashboard')
|
|
businessunitid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('businessunits.businessunitid'),
|
|
nullable=True
|
|
)
|
|
description = db.Column(db.String(255))
|
|
|
|
businessunit = db.relationship('BusinessUnit')
|
|
|
|
@property
|
|
def displaypath(self):
|
|
# Resolve through the normalizer so a legacy or oddly-cased stored value
|
|
# still finds its path instead of silently returning None.
|
|
return DISPLAY_ROLE_PATHS.get(normalize_display_role(self.displayrole))
|
|
|
|
def __repr__(self):
|
|
return f"<DashboardDefault {self.fqdn or self.ipaddress} -> {self.displayrole}>"
|