1019 lines
37 KiB
Python
Executable File
1019 lines
37 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
|
|
#
|
|
# Dieses Tool kann die Websocket-Schnittstelle vom InfrastructureManagement bedienen.
|
|
# Bisher sind folgende Funktionalitäten implementiert:
|
|
# - Man kann sich eine Übersicht über die Gebäude, deren MountingPoints und der Assets geben lassen.
|
|
# - Man kann Assets mounten und unmounten
|
|
#
|
|
# Für Hilfe:
|
|
# $ ism_client.py --help
|
|
# $ ism_client.py <subcommand> --help
|
|
#
|
|
# @author Christian Fröbel
|
|
#
|
|
|
|
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
|
from websocket import WebSocketApp, ABNF
|
|
from uuid import uuid1
|
|
import json
|
|
#import traceback
|
|
import functools
|
|
from io import StringIO
|
|
from queue import Queue
|
|
from threading import Thread
|
|
from contextlib import contextmanager
|
|
import sys
|
|
|
|
LIGHT_GREEN = '\x1b[32;1m'
|
|
LIGHT_PURPLE = '\x1b[35;1m'
|
|
RESET = '\x1b[0m'
|
|
|
|
IN_MARKER = '{}<=={}'.format(LIGHT_PURPLE, RESET)
|
|
OUT_MARKER = '{}==>{}'.format(LIGHT_GREEN, RESET)
|
|
|
|
class Cache(object):
|
|
# @param entity: String, der den Datentyp repräsentiert; nur zu Debugging-Zwecken.
|
|
def __init__(self, entity):
|
|
self._entity = entity
|
|
self._cache = {} # id -> item
|
|
|
|
def register(self, item):
|
|
assert item.id not in self._cache, 'Item is registered already'
|
|
self._cache[item.id] = item
|
|
|
|
def findById(self, id_):
|
|
try:
|
|
return self._cache[id_]
|
|
except KeyError:
|
|
assert False, 'Invalid {}-Id: {}'.format(self._entity, id_)
|
|
|
|
def findByName(self, name):
|
|
foundItem = None
|
|
for item in self._cache.values():
|
|
if item.name == name:
|
|
assert foundItem is None, 'Name of {} is not unique: {}'.format(self._entity, name)
|
|
foundItem = item
|
|
assert foundItem is not None, 'No {} with that name: {}'.format(self._entity, name)
|
|
return foundItem
|
|
|
|
def findAll(self):
|
|
return self._cache.values()
|
|
|
|
def __len__(self):
|
|
return len(self._cache)
|
|
|
|
|
|
buildings = Cache('Building')
|
|
mountingPoints = Cache('MountingPoint')
|
|
assets = Cache('Asset')
|
|
assetTypes = Cache('AssetType')
|
|
mountingPlans = Cache('Plan')
|
|
|
|
|
|
class Building(object):
|
|
@staticmethod
|
|
def create(id_, name, mseId):
|
|
building = Building(id_, name, mseId)
|
|
buildings.register(building)
|
|
return building
|
|
|
|
def __init__(self, id_, name, mseId):
|
|
self._id = id_
|
|
self._name = name
|
|
self._mseId = mseId
|
|
self._mountingPoints = []
|
|
|
|
#
|
|
# Die Verknüpfung ist bidirektional und wird implizit beim Erzeugen des MountingPoints angelegt.
|
|
#
|
|
def addMountingPoint(self, mountingPoint):
|
|
self._mountingPoints.append(mountingPoint)
|
|
|
|
def mountingPoints(self):
|
|
return iter(self._mountingPoints)
|
|
|
|
def findMountingPointByName(self, name):
|
|
found = None
|
|
for mp in self._mountingPoints:
|
|
if mp.name != name:
|
|
continue
|
|
assert found is None, 'Name of MountingPoint is not unique in Building {}: {}'.format(self._name, name)
|
|
found = mp
|
|
assert found is not None, 'No MountingPoint with that name in Building {}:{}'.format(self._name, name)
|
|
return found
|
|
|
|
@property
|
|
def id(self): return self._id
|
|
@property
|
|
def name(self): return self._name
|
|
@property
|
|
def mseId(self): return self._mseId
|
|
|
|
def __str__(self):
|
|
s = StringIO()
|
|
mseId = ''
|
|
if self._mseId is not None:
|
|
mseId = ', mse: {}'.format(self._mseId)
|
|
s.write('Building: {}({}{})\n'.format(self._name, self._id, mseId))
|
|
for mp in sorted(self._mountingPoints, key=lambda mp: mp.type):
|
|
s.write(' {}\n'.format(mp))
|
|
return s.getvalue()
|
|
|
|
|
|
class MountingPoint(object):
|
|
@staticmethod
|
|
def create(id_, name, buildingId, assetTypeId):
|
|
mountingPoint = MountingPoint(id_, name, buildingId, assetTypeId)
|
|
mountingPoints.register(mountingPoint)
|
|
return mountingPoint
|
|
|
|
def __init__(self, id_, name, buildingId, assetTypeId):
|
|
self._id = id_
|
|
self._name = name
|
|
self._building = buildings.findById(buildingId)
|
|
self._building.addMountingPoint(self)
|
|
self._assetType = assetTypes.findById(assetTypeId)
|
|
self._mountedAsset = None # optional
|
|
|
|
#
|
|
# Die Verknüpfung ist bidirektional und wird implizit beim Erzeugen des Assets angelegt.
|
|
#
|
|
def mountAsset(self, asset):
|
|
self._mountedAsset = asset
|
|
|
|
@property
|
|
def id(self): return self._id
|
|
@property
|
|
def name(self): return self._name
|
|
@property
|
|
def type(self): return self._assetType
|
|
@property
|
|
def isMounted(self): return self._mountedAsset is not None
|
|
@property
|
|
def building(self): return self._building
|
|
|
|
@property
|
|
def asset(self):
|
|
assert self.isMounted, 'MountingPoint is not mounted'
|
|
return self._mountedAsset
|
|
|
|
def __str__(self):
|
|
s = 'MountingPoint: {}({}), {},'.format(self._name, self._id, self._assetType)
|
|
if self._mountedAsset is None:
|
|
s += ' not mounted'
|
|
else:
|
|
s += ' {}mounted{}: {}({})'.format(LIGHT_GREEN, RESET, self._mountedAsset.name, self._mountedAsset.id)
|
|
return s
|
|
|
|
|
|
class Asset(object):
|
|
@staticmethod
|
|
def create(id_, name, assetTypeId, mountingPointId):
|
|
asset = Asset(id_, name, assetTypeId, mountingPointId)
|
|
assets.register(asset)
|
|
return asset
|
|
|
|
def __init__(self, id_, name, assetTypeId, mountingPointId):
|
|
self._id = id_
|
|
self._name = name
|
|
self._assetType = assetTypes.findById(assetTypeId)
|
|
|
|
self._mountedOn = None # optional, der MountingPoint, an dem das Asset gemountet ist
|
|
if mountingPointId != -1:
|
|
self._mountedOn = mountingPoints.findById(mountingPointId)
|
|
self._mountedOn.mountAsset(self)
|
|
|
|
@property
|
|
def id(self): return self._id
|
|
@property
|
|
def name(self): return self._name
|
|
@property
|
|
def type(self): return self._assetType
|
|
@property
|
|
def isMounted(self): return self._mountedOn is not None
|
|
|
|
@property
|
|
def mountingPoint(self):
|
|
assert self.isMounted, 'Asset is not mounted'
|
|
return self._mountedOn
|
|
|
|
def __str__(self):
|
|
s = 'Asset: {}({}), {},'.format(self._name, self._id, self._assetType)
|
|
if self._mountedOn is None:
|
|
s += ' not mounted'
|
|
else:
|
|
s += ' {}mounted{}: {}({}) [{}]'.format(LIGHT_GREEN, RESET, self._mountedOn.name, self._mountedOn.id, self._mountedOn.building.name)
|
|
return s
|
|
|
|
|
|
class AssetType(object):
|
|
@staticmethod
|
|
def create(id_, name):
|
|
assetType = AssetType(id_, name)
|
|
assetTypes.register(assetType)
|
|
return assetType
|
|
|
|
def __init__(self, id_, name):
|
|
self._id = id_
|
|
self._name = name
|
|
|
|
@property
|
|
def id(self): return self._id
|
|
@property
|
|
def name(self): return self._name
|
|
|
|
def __str__(self):
|
|
return 'AssetType: {}'.format(self._name)
|
|
|
|
def __lt__(self, other):
|
|
return self._id < other._id
|
|
|
|
|
|
#
|
|
# Der Einrüstplan.
|
|
# Er enthält eine Liste von MountingPoints, die eingerüstet werden sollen.
|
|
#
|
|
class MountingPlan(object):
|
|
@staticmethod
|
|
def create(id_, name):
|
|
plan = MountingPlan(id_, name)
|
|
mountingPlans.register(plan)
|
|
return plan
|
|
|
|
def __init__(self, id_, name):
|
|
self._id = id_
|
|
self._name = name
|
|
self._mountingPoints = []
|
|
|
|
def add(self, mountingPoints):
|
|
self._mountingPoints += mountingPoints
|
|
# Eindeutigkeit sicherstellen
|
|
uniqueMountingPoints = set(self._mountingPoints)
|
|
assert len(uniqueMountingPoints) == len(self._mountingPoints)
|
|
|
|
@property
|
|
def id(self): return self._id
|
|
@property
|
|
def name(self): return self._name
|
|
|
|
def __str__(self):
|
|
byBuilding = {} # Building -> [MountingPoint, ...]
|
|
for mp in self._mountingPoints:
|
|
building = mp.building
|
|
if building in byBuilding:
|
|
byBuilding[building].append(mp)
|
|
else:
|
|
byBuilding[building] = [mp]
|
|
|
|
s = StringIO()
|
|
s.write('Plan: {}({})\n'.format(self._name, self._id))
|
|
for building in byBuilding:
|
|
s.write(' {}({})\n'.format(building.name, building.id))
|
|
for mp in sorted(byBuilding[building], key=lambda mp: mp.name):
|
|
s.write(' {}\n'.format(mp))
|
|
|
|
return s.getvalue()
|
|
|
|
|
|
#
|
|
# So eine Klasse kapselt einfach nur den Zugriff auf ein Dictionary, dessen Keys
|
|
# Strings sind, die außerdem valide Identifier darstellen: die Keys werden Attribute der GeneralConfig-Instanz.
|
|
# @code
|
|
# config = DictAccess({'HOME': '/path/to/home', 'USER': 'froebi'})
|
|
# config.HOME # => '/path/to/name'
|
|
# @endcode
|
|
#
|
|
class DictAccess(object):
|
|
|
|
#
|
|
# @param definedVariables: Ein Dictionary mit den Variablennamen als Keys und deren Wert als Values.
|
|
#
|
|
def __init__(self, definedVariables):
|
|
self._variables = definedVariables
|
|
|
|
def __getattribute__(self, name):
|
|
if name == '_variables':
|
|
return object.__getattribute__(self, name)
|
|
try:
|
|
return self._variables[name]
|
|
except KeyError:
|
|
return object.__getattribute__(self, name)
|
|
|
|
|
|
class WsClient(object):
|
|
def __init__(self, host, port, messageQueue, doLog):
|
|
self._doLog = doLog
|
|
self._messageQueue = messageQueue
|
|
self._ws = WebSocketApp('ws://{}:{}'.format(host, port), on_open=self._onOpen, on_close=self._onClose, on_data=self._onData, on_error=self._onError)
|
|
|
|
def run(self):
|
|
self._ws.run_forever(ping_timeout=1)
|
|
|
|
def close(self):
|
|
# Komischerweise dauert das Schließen des WebSockets manchmal etwas länger.
|
|
# Intern wird noch ein Close-Frame geschickt und auf die Antwort gewartet.
|
|
# Wie lange gewartet wird, sollte man über den Timeout steuern können;
|
|
# das scheint aber nicht zuverlässig zu funktionieren.
|
|
# Wie es aussieht, hängt er im select() (websocket/_app.py), wo ein Timeout von 10 angegeben ist.
|
|
# Der Timeout im select() ist der als ping_timeout übergebene Parameter von run_forever().
|
|
#
|
|
# Das Problem scheint vielmehr darin begründet zu sein, dass wir das close() aus einem anderen
|
|
# Thread heraus machen als dem, in dem run_forever() läuft.
|
|
# Intern wird bei close() tatsächlich auch der Socket gelesen; der eine Thread hängt also im select(),
|
|
# der andere macht das recv().
|
|
self._ws.close(timeout=1)
|
|
|
|
#
|
|
# @param commandType: z.B. 'mount'
|
|
# @param data: das data-Property des Commands JSON-kompatibler Typ -- also z.B dict oder None.
|
|
# @return Die UUID, die für das Command vergeben wurde.
|
|
#
|
|
def sendCommand(self, commandType, data):
|
|
uuid = uuid1()
|
|
command = {'type': commandType, 'uuid': str(uuid), 'data': data}
|
|
if self._doLog:
|
|
print('{} {}: {} '.format(OUT_MARKER, command['type'], json.dumps(command)))
|
|
self._ws.send(json.dumps(command), ABNF.OPCODE_TEXT)
|
|
return uuid
|
|
|
|
def _onOpen(self, ws):
|
|
#
|
|
# Achtung: spezielle interne Message, die dazu verwendet wird, dem
|
|
# Consumer zu sagen, dass wir verbunden sind.
|
|
#
|
|
self._messageQueue.put('connected')
|
|
|
|
def _onClose(self, ws):
|
|
pass
|
|
|
|
#
|
|
# Wenn opcode == TEXT, dann ist data ein Unicode-String,
|
|
# sonst ist data ein Byte-String.
|
|
#
|
|
def _onData(self, ws, data, opcode, fin):
|
|
# print('_onData: opcode: {}, data: {}, fin: {}'.format(ABNF.OPCODE_MAP[opcode], data, fin))
|
|
|
|
# Workaround für lange Messages, die in mehr als einem Frame gesendet werden:
|
|
# Wir bekommen hier offensichtlich schon die zusammengesetzten Daten, aber der opcode ist falsch: der ist dann OPCODE_CONT.
|
|
# fin ist also immer True. Das Problem ist, dass man eben nicht mehr erkennen kann, ob es sich um Text oder Binary Data handelt.
|
|
# Wir gehen hier jetzt einfach davon aus, dass es immer Text ist.
|
|
assert fin, 'currently only atomic messages are supported'
|
|
assert opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_CONT), 'currently only text messages supported'
|
|
|
|
jsonObject = json.loads(data)
|
|
assert isinstance(jsonObject, dict), 'data must be a dictionary'
|
|
messageType = jsonObject['type']
|
|
if self._doLog:
|
|
print('{} {}: {}'.format(IN_MARKER, messageType, data))
|
|
self._messageQueue.put(jsonObject)
|
|
|
|
def _onError(self, ws, exception):
|
|
#traceback.print_exception(exception.__class__, exception, exception.__traceback__, sys.stdout)
|
|
print('{}: {}'.format(exception.__class__.__name__, exception))
|
|
|
|
|
|
#
|
|
# Eine kleine Hilfsklasse, die auf einfache Weise hilft, zu erkennen,
|
|
# wann der Initialisierungsprozess abgeschlossen ist.
|
|
# Nach getEssentials bekommen wir (u.a.) die AssetTypes.
|
|
# Für jeden AssetType fragen wir alle Assets an (getAssets).
|
|
# Wenn wir zu jeder Anfrage die Antworte gesehen haben,
|
|
# sind wir fertig.
|
|
#
|
|
class InitHelper(object):
|
|
def __init__(self):
|
|
self._pendingRequestCount = 0
|
|
|
|
@property
|
|
def pendingCount(self):
|
|
return self._pendingRequestCount
|
|
|
|
# Muss man rufen, wenn man eine neue Anfrage rausgeschickt hat.
|
|
def push(self):
|
|
self._pendingRequestCount += 1
|
|
|
|
# Muss man rufen, wenn man eine Antwort bekommen hat.
|
|
# Liefert True, wenn fertig.
|
|
def pop(self):
|
|
self._pendingRequestCount -= 1
|
|
return self._pendingRequestCount == 0
|
|
|
|
|
|
|
|
#
|
|
# Der Initializer liest Commands aus der CommandQueue und baut daraus
|
|
# die Strukturen für Buildings, MountingPoints und Assets auf.
|
|
# Dazu macht er auch eigene Anfragen an den Server.
|
|
# Wenn er fertig ist, beendet sich die run()-Methode.
|
|
#
|
|
class Initializer(object):
|
|
def __init__(self, messageQueue, sender):
|
|
self._sender = sender
|
|
self._messageQueue = messageQueue
|
|
self._initHelper = InitHelper()
|
|
self._finished = False
|
|
|
|
def run(self):
|
|
self._waitForConnected()
|
|
while not self._finished:
|
|
self._processMessage()
|
|
|
|
#
|
|
# Die erste Message in der Queue ist ein internes Command -- einfach nur ein String.
|
|
# Das ist für uns das Signal, dass wir mit dem Server verbunden sind und
|
|
# die Initialisierung via getEssentials starten können.
|
|
#
|
|
def _waitForConnected(self):
|
|
internalCommand = self._messageQueue.get()
|
|
assert internalCommand == 'connected'
|
|
self._getEssentials()
|
|
|
|
def _processMessage(self):
|
|
jsonObject = self._messageQueue.get()
|
|
messageType = jsonObject['type']
|
|
|
|
if messageType == 'essentials':
|
|
self._handleEssentials(jsonObject)
|
|
elif messageType == 'assets':
|
|
self._handleAssets(jsonObject)
|
|
elif messageType == 'error':
|
|
print('Service responded with error: {}'.format(jsonObject['data']['errorMessage']))
|
|
sys.exit(1)
|
|
else:
|
|
assert False, 'unknown/unhandled messageType: {}'.format(messageType)
|
|
|
|
#
|
|
# Mittlerweile antwortet der Server auf getEssentials mit nur einem Telegramm -- essentials --,
|
|
# welches die vorherigen Einzeltelegramme einfach zusammenfasst.
|
|
#
|
|
def _handleEssentials(self, jsonObject):
|
|
data = DictAccess(jsonObject['data'])
|
|
buildings = data.objects
|
|
mountingPoints = data.mountingPoints
|
|
assetTypes = data.assetTypes
|
|
|
|
for b in buildings:
|
|
self._handleObject(b)
|
|
|
|
# AssetTypes vor den MountingPoints behandeln, denn das Erzeugen der MountingPoints
|
|
# setzt die AssetTypes voraus.
|
|
for t in assetTypes:
|
|
self._handleAssetType(t)
|
|
|
|
for mp in mountingPoints:
|
|
self._handleMountingPoint(mp)
|
|
|
|
|
|
#
|
|
# object wird als Antwort auf getEssentials gesendet.
|
|
#
|
|
def _handleObject(self, dataObject):
|
|
data = DictAccess(dataObject)
|
|
buildingId = data.id
|
|
name = data.designation
|
|
try:
|
|
mseId = data.mseId
|
|
except KeyError:
|
|
mseId = None
|
|
building = Building.create(buildingId, name, mseId)
|
|
|
|
#
|
|
# Hier bekommen wir alle MountingPoints -- auch die nicht gemounteten.
|
|
#
|
|
def _handleMountingPoint(self, dataObject):
|
|
data = DictAccess(dataObject)
|
|
mountingPointId = data.id
|
|
name = data.designation
|
|
buildingId = data.objectId
|
|
supportedAssetTypeIds = data.supportedAssetTypeIds
|
|
assert len(supportedAssetTypeIds)
|
|
mountingPoint = MountingPoint.create(mountingPointId, name, buildingId, supportedAssetTypeIds[0])
|
|
|
|
#
|
|
# Als Antwort auf getEssentials bekommen wir auch eine Liste der vorhandenen Asset-Typen.
|
|
# Daraufhin fragen wir alle Assets von diesem Typ an.
|
|
#
|
|
def _handleAssetType(self, dataObject):
|
|
data = DictAccess(dataObject)
|
|
assetTypeId = data.id
|
|
name = data.designation
|
|
AssetType.create(assetTypeId, name)
|
|
self._getAssets(assetTypeId)
|
|
self._initHelper.push()
|
|
|
|
#
|
|
# Antwort auf getAssets
|
|
#
|
|
def _handleAssets(self, jsonObject):
|
|
data = jsonObject['data']
|
|
for asset in [DictAccess(d) for d in data]:
|
|
assetId = asset.id
|
|
assetTypeId = asset.assetTypeId
|
|
name = asset.designation
|
|
mountingPointId = asset.mountingPointId
|
|
asset = Asset.create(assetId, name, assetTypeId, mountingPointId)
|
|
|
|
self._finished = self._initHelper.pop()
|
|
|
|
#
|
|
# getEssentials ist der Einstiegspunkt.
|
|
# Der Server antwortet darauf mit einer Message, die folgende Dinge enthält:
|
|
# - trainingArea
|
|
# - area
|
|
# - object
|
|
# - level
|
|
# - room
|
|
# - mountingPoint
|
|
# - assetType
|
|
# Im Moment interessieren wir uns nur für AssetType, Object (Building) und MountingPoint
|
|
#
|
|
def _getEssentials(self):
|
|
self._sender.sendCommand('getEssentials', None)
|
|
|
|
def _getAssets(self, assetTypeId):
|
|
data = {'assetTypeId': assetTypeId}
|
|
self._sender.sendCommand('getAssets', data)
|
|
|
|
|
|
#
|
|
# Initialisiert die Strukturen für die Einrüstpläne.
|
|
# Die run()-Methode beendet sich, wenn die Initialisierung abgeschlossen ist.
|
|
#
|
|
class PlanInitializer(object):
|
|
def __init__(self, messageQueue, client):
|
|
self._messageQueue = messageQueue
|
|
self._client = client
|
|
self._initHelper = InitHelper()
|
|
self._finished = False
|
|
|
|
def run(self):
|
|
self._getPlannedMounting()
|
|
while not self._finished:
|
|
self._processMessage()
|
|
|
|
def _getPlannedMounting(self):
|
|
self._client.sendCommand('getPlannedMountings', None)
|
|
|
|
def _getObjectMounting(self, planId, buildingId):
|
|
data = {'plannedMountingId': planId, 'objectId': buildingId}
|
|
self._client.sendCommand('getObjectMounting', data)
|
|
|
|
def _processMessage(self):
|
|
jsonObject = self._messageQueue.get()
|
|
messageType = jsonObject['type']
|
|
|
|
if messageType == 'plannedMountings':
|
|
self._handlePlannedMountings(jsonObject)
|
|
elif messageType == 'objectMounting':
|
|
self._handleObjectMounting(jsonObject)
|
|
elif messageType == 'error':
|
|
print('Service responded with error: {}'.format(jsonObject['data']['errorMessage']))
|
|
sys.exit(1)
|
|
|
|
def _handlePlannedMountings(self, jsonObject):
|
|
mountings = jsonObject['data']
|
|
for m in mountings:
|
|
self._handlePlannedMounting(m)
|
|
|
|
# Falls es keine Pläne gibt oder alle Pläne noch leer sind, werden ja keine
|
|
# getObjectMounting-Anfragen gemacht. Für den Fall müssen wir hier explizit
|
|
# self._finished auf True setzen.
|
|
if self._initHelper.pendingCount == 0:
|
|
self._finished = True
|
|
|
|
def _handlePlannedMounting(self, dataObject):
|
|
data = DictAccess(dataObject)
|
|
planId = data.plannedMountingId
|
|
name = data.designation
|
|
MountingPlan.create(planId, name)
|
|
|
|
buildings = data.mounting
|
|
for building in buildings:
|
|
buildingId = building['id']
|
|
self._getObjectMounting(planId, buildingId)
|
|
self._initHelper.push()
|
|
|
|
def _handleObjectMounting(self, jsonObject):
|
|
data = jsonObject['data']
|
|
plan = mountingPlans.findById(data['plannedMountingId'])
|
|
mps = []
|
|
for mp in data['planned']:
|
|
mps.append(mountingPoints.findById(mp['id']))
|
|
plan.add(mps)
|
|
self._finished = self._initHelper.pop()
|
|
|
|
|
|
#
|
|
# Hilfsklasse für das Abfragen des ISM-Ports über das WebSocket-Interface
|
|
# des ServiceManagements.
|
|
#
|
|
class IsmPortFinder(object):
|
|
def __init__(self, messageQueue, client):
|
|
self._messageQueue = messageQueue
|
|
self._sender = client
|
|
self._ismPort = None
|
|
self._finished = False
|
|
|
|
@property
|
|
def port(self):
|
|
assert self._ismPort is not None
|
|
return self._ismPort
|
|
|
|
def run(self):
|
|
self._waitForConnected()
|
|
while not self._finished:
|
|
self._processMessage()
|
|
|
|
#
|
|
# Die erste Message in der Queue ist ein internes Command -- einfach nur ein String.
|
|
# Das ist für uns das Signal, dass wir mit dem Server verbunden sind und
|
|
# die Initialisierung via getEssentials starten können.
|
|
#
|
|
def _waitForConnected(self):
|
|
internalCommand = self._messageQueue.get()
|
|
assert internalCommand == 'connected'
|
|
self._getPortInfo()
|
|
|
|
def _processMessage(self):
|
|
jsonObject = self._messageQueue.get()
|
|
messageType = jsonObject['type']
|
|
assert messageType == 'servicePortsInformation'
|
|
self._handlePortInfo(jsonObject)
|
|
|
|
def _getPortInfo(self):
|
|
self._sender.sendCommand('servicePortsInformationRequest', None)
|
|
|
|
def _handlePortInfo(self, jsonObject):
|
|
data = jsonObject['data']
|
|
for exerciseInfo in [DictAccess(d) for d in data]:
|
|
if exerciseInfo.exerciseId != 0:
|
|
continue
|
|
for serviceInfo in [DictAccess(d) for d in exerciseInfo.services]:
|
|
if serviceInfo.nameAbbrevation != 'ism':
|
|
continue
|
|
assert len(serviceInfo.ports) == 1 # Wie finden wir den gewünschten Port, wenn es mehrere gibt?
|
|
self._ismPort = serviceInfo.ports[0]['number']
|
|
break
|
|
assert self._ismPort is not None
|
|
self._finished = True
|
|
|
|
|
|
#
|
|
# Kleine Hilfsklasse, die einfach nur auf die Ok- oder Error-Reponse
|
|
# wartet, die z.B. auf ein mount-Command gesendet wird.
|
|
#
|
|
class ResponseWaiter(object):
|
|
#
|
|
# @param uuid: die UUID des Commands, auf dessen Response wir warten.
|
|
#
|
|
def __init__(self, messageQueue, uuid):
|
|
self._messageQueue = messageQueue
|
|
self._uuid = uuid
|
|
|
|
def wait(self):
|
|
while True:
|
|
jsonObject = self._messageQueue.get()
|
|
messageType = jsonObject['type']
|
|
data = DictAccess(jsonObject['data'])
|
|
if data.uuid != str(self._uuid):
|
|
continue
|
|
if messageType == 'ok':
|
|
return True, data.info
|
|
if messageType == 'error':
|
|
return False, data.errorMessage
|
|
assert False, 'Response must be either ok or error'
|
|
|
|
|
|
@contextmanager
|
|
def autoClose(client):
|
|
try:
|
|
yield client
|
|
finally:
|
|
client.close()
|
|
|
|
@contextmanager
|
|
def autoJoin(thread):
|
|
try:
|
|
yield thread
|
|
finally:
|
|
thread.join()
|
|
|
|
|
|
def _mainList(args, messageQueue, client):
|
|
for b in sorted(buildings.findAll(), key=lambda b: b.name):
|
|
mseId = ''
|
|
if b.mseId is not None:
|
|
mseId = ', mseId: {}'.format(b.mseId)
|
|
print('Building: {}({}{})'.format(b.name, b.id, mseId))
|
|
return 0
|
|
|
|
|
|
def _mainListBuildingByName(args, messageQueue, client):
|
|
print(buildings.findByName(args.building))
|
|
return 0
|
|
|
|
|
|
def _mainListBuildingById(args, messageQueue, client):
|
|
print(buildings.findById(args.buildingId))
|
|
return 0
|
|
|
|
|
|
def _mainListAssets(args, messageQueue, client):
|
|
for a in sorted(assets.findAll(), key=lambda a: a.type):
|
|
print(a)
|
|
return 0
|
|
|
|
|
|
def _mainMountByName(args, messageQueue, client):
|
|
building = None
|
|
if args.building is not None:
|
|
building = buildings.findByName(args.building)
|
|
return _mountByName(args.asset, args.mountingPoint, building, messageQueue, client)
|
|
|
|
|
|
def _mainMountById(args, messageQueue, client):
|
|
asset = assets.findById(args.assetId)
|
|
mountingPoint = mountingPoints.findById(args.mountingPointId)
|
|
return _mount(asset, mountingPoint, messageQueue, client)
|
|
|
|
|
|
def _mainMountBarcode(args, messageQueue, client):
|
|
data = {'assetTag': args.asset, 'mountingPointTag': args.mountingPoint}
|
|
uuid = client.sendCommand('mountTag', data)
|
|
|
|
waiter = ResponseWaiter(messageQueue, uuid)
|
|
return _waitForRequest(waiter, 'mounted successfully', 'mounting failed')
|
|
|
|
|
|
def _mainMountManyByName(args, messageQueue, client):
|
|
building = None
|
|
if args.building is not None:
|
|
building = buildings.findByName(args.building)
|
|
jsonList = json.loads(args.json)
|
|
for assetMountingPoint in jsonList:
|
|
assetName = assetMountingPoint[0]
|
|
mountingPointName = assetMountingPoint[1]
|
|
status = _mountByName(assetName, mountingPointName, building, messageQueue, client)
|
|
if status != 0:
|
|
return status
|
|
return 0
|
|
|
|
|
|
def _findMountingPointByName(name, buildingContext=None):
|
|
if buildingContext is None:
|
|
return mountingPoints.findByName(name)
|
|
else:
|
|
return buildingContext.findMountingPointByName(name)
|
|
|
|
def _mountByName(assetName, mountingPointName, building, messageQueue, client):
|
|
asset = assets.findByName(assetName)
|
|
mountingPoint = _findMountingPointByName(mountingPointName, building)
|
|
return _mount(asset, mountingPoint, messageQueue, client)
|
|
|
|
def _mount(asset, mountingPoint, messageQueue, client):
|
|
print('mounting {}({}) -> {}({})'.format(asset.name, asset.id, mountingPoint.name, mountingPoint.id))
|
|
|
|
data = {'assetId': asset.id, 'mountingPointId': mountingPoint.id}
|
|
uuid = client.sendCommand('mount', data)
|
|
|
|
waiter = ResponseWaiter(messageQueue, uuid)
|
|
return _waitForRequest(waiter, 'mounted successfully', 'mounting failed')
|
|
|
|
|
|
def _mainUnmountAssetByName(args, messageQueue, client):
|
|
asset = assets.findByName(args.asset)
|
|
return _unmountMountingPoint(asset.mountingPoint, messageQueue, client)
|
|
|
|
|
|
def _mainUnmountAssetById(args, messageQueue, client):
|
|
asset = assets.findById(args.assetId)
|
|
return _unmountMountingPoint(asset.mountingPoint, messageQueue, client)
|
|
|
|
|
|
def _mainUnmountMountingPointByName(args, messageQueue, client):
|
|
building = None
|
|
if args.building is not None:
|
|
building = buildings.findByName(args.building)
|
|
mountingPoint = _findMountingPointByName(args.mountingPoint, building)
|
|
return _unmountMountingPoint(mountingPoint, messageQueue, client)
|
|
|
|
|
|
def _mainUnmountMountingPointById(args, messageQueue, client):
|
|
mountingPoint = mountingPoints.findById(args.mountingPointId)
|
|
return _unmountMountingPoint(mountingPoint, messageQueue, client)
|
|
|
|
|
|
def _mainUnmountBuildingByName(args, messageQueue, client):
|
|
building = buildings.findByName(args.building)
|
|
return _unmountBuilding(building, messageQueue, client)
|
|
|
|
|
|
def _mainUnmountBuildingById(args, messageQueue, client):
|
|
building = buildings.findById(args.buildingId)
|
|
return _unmountBuilding(building, messageQueue, client)
|
|
|
|
|
|
def _unmountBuilding(building, messageQueue, client):
|
|
for mountingPoint in building.mountingPoints():
|
|
if not mountingPoint.isMounted:
|
|
continue
|
|
status = _unmountMountingPoint(mountingPoint, messageQueue, client)
|
|
if status != 0:
|
|
return status
|
|
return 0
|
|
|
|
|
|
def _mainUnmountAll(args, messageQueue, client):
|
|
for a in assets.findAll():
|
|
if not a.isMounted:
|
|
continue
|
|
status = _unmountMountingPoint(a.mountingPoint, messageQueue, client)
|
|
if status != 0:
|
|
return status
|
|
return 0
|
|
|
|
|
|
def _unmountMountingPoint(mountingPoint, messageQueue, client):
|
|
asset = mountingPoint.asset
|
|
print('unmounting {}({}) from {}({})'.format(asset.name, asset.id, mountingPoint.name, mountingPoint.id))
|
|
data = {'mountingPointId': mountingPoint.id}
|
|
uuid = client.sendCommand('unmountMountingPoint', data)
|
|
waiter = ResponseWaiter(messageQueue, uuid)
|
|
return _waitForRequest(waiter, 'unmounted successfully', 'unmounting failed')
|
|
|
|
|
|
def _mainListMountingPlans(args, messageQueue, client):
|
|
planInitializer = PlanInitializer(messageQueue, client)
|
|
planInitializer.run()
|
|
|
|
if len(mountingPlans) == 0:
|
|
print('No mounting plans available')
|
|
return 0
|
|
|
|
for p in mountingPlans.findAll():
|
|
print('Plan: {}({})'.format(p.name, p.id))
|
|
return 0
|
|
|
|
|
|
def _mainListMountingPlanByName(args, messageQueue, client):
|
|
planInitializer = PlanInitializer(messageQueue, client)
|
|
planInitializer.run()
|
|
|
|
plan = mountingPlans.findByName(args.plan)
|
|
print(plan)
|
|
|
|
|
|
def _mainListMountingPlanById(args, messageQueue, client):
|
|
planInitializer = PlanInitializer(messageQueue, client)
|
|
planInitializer.run()
|
|
|
|
plan = mountingPlans.findById(args.planId)
|
|
print(plan)
|
|
|
|
|
|
def _waitForRequest(waiter, okMessage, errorMessage):
|
|
status, info = waiter.wait()
|
|
if status:
|
|
print(okMessage)
|
|
return 0
|
|
else:
|
|
print('{}: {}'.format(errorMessage, info))
|
|
return 1
|
|
|
|
|
|
def _parseCmdLine():
|
|
description = 'hello'
|
|
parser = ArgumentParser(description=description, formatter_class=RawDescriptionHelpFormatter)
|
|
|
|
sharedArgs = ArgumentParser(add_help=False)
|
|
sharedArgs.add_argument('--host', default='localhost', help='hostname/ip; default: localhost')
|
|
sharedArgs.add_argument('--port', type=int, help='ISM port; if not given, the port will be determined via SCM WebSocket interface on port 7300')
|
|
sharedArgs.add_argument('--log-messages', '-l', action='store_true', default=False, dest='doLog', help='log ingoing and outgoing messages')
|
|
|
|
subparsers = parser.add_subparsers()
|
|
|
|
parser_list = subparsers.add_parser('list', parents=[sharedArgs], help='list all buildings', description='List buildings.')
|
|
parser_list.set_defaults(commandFunc=_mainList)
|
|
|
|
parser_listBuildingByName = subparsers.add_parser('list-building', parents=[sharedArgs], help='list a building', description='List all mounting points for a given building.')
|
|
parser_listBuildingByName.add_argument('building', help='name of building')
|
|
parser_listBuildingByName.set_defaults(commandFunc=_mainListBuildingByName)
|
|
|
|
parser_listBuildingById = subparsers.add_parser('list-building-by-id', parents=[sharedArgs], help='list a building by ID', description='List all mounting points for a given building.')
|
|
parser_listBuildingById.add_argument('buildingId', type=int, help='building ID')
|
|
parser_listBuildingById.set_defaults(commandFunc=_mainListBuildingById)
|
|
|
|
parser_listAssets = subparsers.add_parser('list-assets', parents=[sharedArgs], help='list all assets', description='List all assets.')
|
|
parser_listAssets.set_defaults(commandFunc=_mainListAssets)
|
|
|
|
|
|
parser_mountByName = subparsers.add_parser('mount', parents=[sharedArgs], help='mount asset', description='Mount an asset to a mounting point by name.')
|
|
parser_mountByName.add_argument('asset', help='name of asset')
|
|
parser_mountByName.add_argument('mountingPoint', help='name of mounting point')
|
|
parser_mountByName.add_argument('--building', help='search for mounting point in the building with that name in case the name is not globally unique')
|
|
parser_mountByName.set_defaults(commandFunc=_mainMountByName)
|
|
|
|
parser_mountById = subparsers.add_parser('mount-by-id', parents=[sharedArgs], help='mount by ID', description='Mount an asset to a mounting point by ID')
|
|
parser_mountById.add_argument('assetId', type=int, help='ID of asset')
|
|
parser_mountById.add_argument('mountingPointId', type=int, help='ID of mounting point')
|
|
parser_mountById.set_defaults(commandFunc=_mainMountById)
|
|
|
|
parser_mountBarcode = subparsers.add_parser('mount-barcode', parents=[sharedArgs], help='mount asset via barcode', description='Mount an asset to a mounting point by barcode.')
|
|
parser_mountBarcode.add_argument('asset', help='barcode of asset')
|
|
parser_mountBarcode.add_argument('mountingPoint', help='barcode of mounting point')
|
|
parser_mountBarcode.set_defaults(commandFunc=_mainMountBarcode)
|
|
|
|
parser_mountManyByName = subparsers.add_parser('mount-many', parents=[sharedArgs], help='mount many assets', description='Mount several assets to mounting points at once using their names.')
|
|
parser_mountManyByName.add_argument('json', help='JSON list of asset-mounting point pairs, e.g. [ ["asset1", "mp1"], ["asset2", "mp2"] ]')
|
|
parser_mountManyByName.add_argument('--building', help='search for mounting point in the building with that name in case the name is not globally unique')
|
|
parser_mountManyByName.set_defaults(commandFunc=_mainMountManyByName)
|
|
|
|
parser_unmountAssetByName = subparsers.add_parser('unmount-asset', parents=[sharedArgs], help='unmout asset', description='Unmount an asset by name.')
|
|
parser_unmountAssetByName.add_argument('asset', help='name of asset')
|
|
parser_unmountAssetByName.set_defaults(commandFunc=_mainUnmountAssetByName)
|
|
|
|
parser_unmountAssetById = subparsers.add_parser('unmount-asset-by-id', parents=[sharedArgs], help='unmout asset by ID', description='Unmount an asset by ID.')
|
|
parser_unmountAssetById.add_argument('assetId', type=int, help='ID of asset')
|
|
parser_unmountAssetById.set_defaults(commandFunc=_mainUnmountAssetById)
|
|
|
|
parser_unmountMountingPointByName = subparsers.add_parser('unmount-mounting-point', parents=[sharedArgs], help='unmout mounting point', description='Unmount a mounting point by name.')
|
|
parser_unmountMountingPointByName.add_argument('mountingPoint', help='name of mounting point')
|
|
parser_unmountMountingPointByName.add_argument('--building', help='search for mounting point in the building with that name in case the name is not globally unique')
|
|
parser_unmountMountingPointByName.set_defaults(commandFunc=_mainUnmountMountingPointByName)
|
|
|
|
parser_unmountMountingPointById = subparsers.add_parser('unmount-mounting-point-by-id', parents=[sharedArgs], help='unmout mounting point', description='Unmount a mounting point by ID.')
|
|
parser_unmountMountingPointById.add_argument('mountingPointId', type=int, help='ID of mounting point')
|
|
parser_unmountMountingPointById.set_defaults(commandFunc=_mainUnmountMountingPointById)
|
|
|
|
parser_unmountBuildingByName = subparsers.add_parser('unmount-building', parents=[sharedArgs], help='unmount building', description='Unmount all assets in a building.')
|
|
parser_unmountBuildingByName.add_argument('building', help='name of building')
|
|
parser_unmountBuildingByName.set_defaults(commandFunc=_mainUnmountBuildingByName)
|
|
|
|
parser_unmountBuildingById = subparsers.add_parser('unmount-building-by-id', parents=[sharedArgs], help='unmount building by ID', description='Unmount all assets in a building.')
|
|
parser_unmountBuildingById.add_argument('buildingId', type=int, help='ID of building')
|
|
parser_unmountBuildingById.set_defaults(commandFunc=_mainUnmountBuildingById)
|
|
|
|
parser_unmountAll = subparsers.add_parser('unmount-all', parents=[sharedArgs], help='unmount all assets', description='Unmount all assets.')
|
|
parser_unmountAll.set_defaults(commandFunc=_mainUnmountAll)
|
|
|
|
|
|
parser_listPlans = subparsers.add_parser('list-plans', parents=[sharedArgs], help='list available mounting plans', description='List available mounting plans')
|
|
parser_listPlans.set_defaults(commandFunc=_mainListMountingPlans)
|
|
|
|
parser_listPlanByName = subparsers.add_parser('list-plan', parents=[sharedArgs], help='list a mounting plan', description='List a mounting plan.')
|
|
parser_listPlanByName.add_argument('plan', help='name of plan')
|
|
parser_listPlanByName.set_defaults(commandFunc=_mainListMountingPlanByName)
|
|
|
|
parser_listPlanById = subparsers.add_parser('list-plan-by-id', parents=[sharedArgs], help='list a mounting plan by ID', description='List a mounting plan by ID.')
|
|
parser_listPlanById.add_argument('planId', type=int, help='ID of plan')
|
|
parser_listPlanById.set_defaults(commandFunc=_mainListMountingPlanById)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
#
|
|
# Herausfinden des ISM-Ports über das Abfragen der WebSocket-Schnittstelle
|
|
# des ServiceManagements.
|
|
#
|
|
def _determineIsmPort(args):
|
|
messageQueue = Queue()
|
|
client = WsClient(args.host, 7300, messageQueue, args.doLog) # SCM-Port is fix
|
|
|
|
portFinder = IsmPortFinder(messageQueue, client)
|
|
|
|
clientThread = Thread(target=client.run)
|
|
clientThread.start()
|
|
|
|
with autoJoin(clientThread):
|
|
with autoClose(client):
|
|
portFinder.run() # beendet sich, wenn der Port gefunden wurde
|
|
|
|
return portFinder.port
|
|
|
|
|
|
def _main():
|
|
args = _parseCmdLine()
|
|
|
|
ismPort = args.port
|
|
if ismPort is None:
|
|
ismPort = _determineIsmPort(args)
|
|
|
|
messageQueue = Queue()
|
|
|
|
client = WsClient(args.host, ismPort, messageQueue, args.doLog)
|
|
initializer = Initializer(messageQueue, client)
|
|
|
|
clientThread = Thread(target=client.run)
|
|
clientThread.start()
|
|
|
|
with autoJoin(clientThread):
|
|
with autoClose(client):
|
|
initializer.run() # beendet sich, wenn initialisiert
|
|
retVal = args.commandFunc(args, messageQueue, client)
|
|
|
|
return retVal
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(_main())
|