From 6878a90ba01bbedce2b643c1137fc5608b742d38 Mon Sep 17 00:00:00 2001 From: Slyke Date: Wed, 6 Nov 2024 01:57:04 -0800 Subject: [PATCH 1/5] Added python-matter-server and thread --- .templates/otbr/build.py | 390 +++++++++++++++++ .templates/otbr/select_hardware.py | 337 ++++++++++++++ .templates/otbr/service.yml | 23 + .templates/otbr/thread_hardware.yml | 11 + .templates/python-matter-server/build.py | 413 ++++++++++++++++++ .../python-matter-server/matter_extras.yml | 7 + .../python-matter-server/select_extras.py | 330 ++++++++++++++ .templates/python-matter-server/service.yml | 15 + 8 files changed, 1526 insertions(+) create mode 100755 .templates/otbr/build.py create mode 100755 .templates/otbr/select_hardware.py create mode 100644 .templates/otbr/service.yml create mode 100644 .templates/otbr/thread_hardware.yml create mode 100755 .templates/python-matter-server/build.py create mode 100644 .templates/python-matter-server/matter_extras.yml create mode 100755 .templates/python-matter-server/select_extras.py create mode 100644 .templates/python-matter-server/service.yml diff --git a/.templates/otbr/build.py b/.templates/otbr/build.py new file mode 100755 index 00000000..bb15b805 --- /dev/null +++ b/.templates/otbr/build.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 + +issues = {} # Returned issues dict +buildHooks = {} # Options, and others hooks +haltOnErrors = True + +# Main wrapper function. Required to make local vars work correctly +def main(): + import os + import time + import ruamel.yaml + import signal + import sys + from blessed import Terminal + + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText + from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName, buildCache, servicesFileName + from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail + + yaml = ruamel.yaml.YAML() + yaml.preserve_quotes = True + + global dockerComposeServicesYaml # The loaded memory YAML of all checked services + global toRun # Switch for which function to run when executed + global buildHooks # Where to place the options menu result + global currentServiceName # Name of the current service + global issues # Returned issues dict + global haltOnErrors # Turn on to allow erroring + global hideHelpText # Showing and hiding the help controls text + global serviceService + global hasRebuiltHardwareSelection + + serviceService = servicesDirectory + currentServiceName + serviceTemplate = templatesDirectory + currentServiceName + buildSettings = serviceService + buildSettingsFileName + + hasRebuiltHardwareSelection = False + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + documentationHint = 'https://openthread.io/guides/border-router/docker' + + # runtime vars + portConflicts = [] + + # This lets the menu know whether to put " >> Options " or not + # This function is REQUIRED. + def checkForOptionsHook(): + try: + buildHooks["options"] = callable(runOptionsMenu) + except: + buildHooks["options"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForPreBuildHook(): + try: + buildHooks["preBuildHook"] = callable(preBuild) + except: + buildHooks["preBuildHook"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForPostBuildHook(): + try: + buildHooks["postBuildHook"] = callable(postBuild) + except: + buildHooks["postBuildHook"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForRunChecksHook(): + try: + buildHooks["runChecksHook"] = callable(runChecks) + except: + buildHooks["runChecksHook"] = False + return buildHooks + return buildHooks + + # This service will not check anything unless this is set + # This function is optional, and will run each time the menu is rendered + def runChecks(): + checkForIssues() + return [] + + # This function is optional, and will run after the docker-compose.yml file is written to disk. + def postBuild(): + return True + + # This function is optional, and will run just before the build docker-compose.yml code. + def preBuild(): + global dockerComposeServicesYaml + global currentServiceName + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objHardwareListFile: + otbrYamlBuildOptions = yaml.load(objHardwareListFile) + + with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: + serviceYamlTemplate = yaml.load(objServiceFile) + + oldBuildCache = {} + try: + with open(r'%s' % buildCache) as objBuildCache: + oldBuildCache = yaml.load(objBuildCache) + except: + pass + + + buildCacheServices = {} + if "services" in oldBuildCache: + buildCacheServices = oldBuildCache["services"] + + if not os.path.exists(serviceService): + os.makedirs(serviceService, exist_ok=True) + + try: + if currentServiceName in dockerComposeServicesYaml: + if otbrYamlBuildOptions["hardware"] and len(otbrYamlBuildOptions["hardware"]) == 1: + newCommand = "--radio-url spinel+hdlc+uart://" + otbrYamlBuildOptions["hardware"][0] + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + dockerComposeServicesYaml[currentServiceName]["volumes"].append(otbrYamlBuildOptions["hardware"][0] + ":" + otbrYamlBuildOptions["hardware"][0]) + + except Exception as err: + print("Error setting otbr hardware: ", err) + time.sleep(10) + return False + + + + + return True + + # ##################################### + # Supporting functions below + # ##################################### + + def checkForIssues(): + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objHardwareListFile: + otbrYamlBuildOptions = yaml.load(objHardwareListFile) + for (index, serviceName) in enumerate(dockerComposeServicesYaml): + if not currentServiceName == serviceName: # Skip self + currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) + portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) + if (len(portConflicts) > 0): + issues["portConflicts"] = portConflicts + + print(otbrYamlBuildOptions["hardware"]) + if not otbrYamlBuildOptions["hardware"] or len(otbrYamlBuildOptions["hardware"]) < 1: + issues["hardware"] = "No Thread radio selected." + if otbrYamlBuildOptions["hardware"] and len(otbrYamlBuildOptions["hardware"]) > 1: + issues["hardware"] = "Two or more thread radios selected. The first listed one will be used" + + + + + # ##################################### + # End Supporting functions + # ##################################### + + + ############################ + # Menu Logic + ############################ + + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + + selectionInProgress = True + currentMenuItemIndex = 0 + menuNavigateDirection = 0 + needsRender = 1 + term = Terminal() + hotzoneLocation = [((term.height // 16) + 6), 0] + + def goBack(): + global selectionInProgress + global needsRender + selectionInProgress = False + needsRender = 1 + return True + + def selectMatterHardware(): + global needsRender + global hasRebuiltHardwareSelection + threadSelectHardwareFilePath = "./.templates/otbr/select_hardware.py" + with open(threadSelectHardwareFilePath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), threadSelectHardwareFilePath, "exec") + # execGlobals = globals() + # execLocals = locals() + execGlobals = { + "currentServiceName": currentServiceName, + "renderMode": renderMode + } + execLocals = {} + screenActive = False + exec(code, execGlobals, execLocals) + signal.signal(signal.SIGWINCH, onResize) + try: + hasRebuiltHardwareSelection = execGlobals["hasRebuiltHardwareSelection"] + except: + hasRebuiltHardwareSelection = False + screenActive = True + needsRender = 1 + + def onResize(sig, action): + global threadBuildOptions + global currentMenuItemIndex + mainRender(1, threadBuildOptions, currentMenuItemIndex) + + threadBuildOptions = [] + + def createMenu(): + global threadBuildOptions + global serviceService + threadBuildOptions = [] + + + if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): + threadBuildOptions.insert(0, ["Change selected hardware", selectMatterHardware]) + else: + threadBuildOptions.insert(0, ["Select hardware", selectMatterHardware]) + + threadBuildOptions.append(["Go back", goBack]) + + def runOptionsMenu(): + createMenu() + menuEntryPoint() + return True + + def renderHotZone(term, menu, selection, hotzoneLocation): + lineLengthAtTextStart = 71 + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + for (index, menuItem) in enumerate(menu): + toPrint = "" + if index == selection: + toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + else: + toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + + for i in range(lineLengthAtTextStart - len(menuItem[0])): + toPrint += " " + + toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) + + toPrint = term.center(toPrint) + + print(toPrint) + + def mainRender(needsRender, menu, selection): + global hasRebuiltHardwareSelection + term = Terminal() + + if needsRender == 1: + print(term.clear()) + print(term.move_y(term.height // 16)) + print(term.black_on_cornsilk4(term.center('IOTstack Thread Options'))) + print("") + print(term.center(commonTopBorder(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if needsRender >= 1: + renderHotZone(term, menu, selection, hotzoneLocation) + + if needsRender == 1: + if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): + if hasRebuiltHardwareSelection: + print(term.center(commonEmptyLine(renderMode))) + print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Hardware list has been rebuilt: build_settings.yml", bv=specialChars[renderMode]["borderVertical"]))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Using existing build_settings.yml for hardware installation", bv=specialChars[renderMode]["borderVertical"]))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} There are extra steps you need to do before Thread will work. Be sure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} to read the documentation. IPv6 and flashing custon firmware on {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} a Thread ready USB radio. {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + if len(documentationHint) > 1: + if len(documentationHint) > 56: + documentationAndPadding = padText(documentationHint, 71) + print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) + else: + documentationAndPadding = padText(documentationHint, 56) + print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + def runSelection(selection): + import types + global threadBuildOptions + if len(threadBuildOptions[selection]) > 1 and isinstance(threadBuildOptions[selection][1], types.FunctionType): + threadBuildOptions[selection][1]() + else: + print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) + + def isMenuItemSelectable(menu, index): + if len(menu) > index: + if len(menu[index]) > 2: + if menu[index][2]["skip"] == True: + return False + return True + + def menuEntryPoint(): + # These need to be reglobalised due to eval() + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + global hideHelpText + global threadBuildOptions + term = Terminal() + with term.fullscreen(): + menuNavigateDirection = 0 + mainRender(needsRender, threadBuildOptions, currentMenuItemIndex) + selectionInProgress = True + with term.cbreak(): + while selectionInProgress: + menuNavigateDirection = 0 + + if needsRender: # Only rerender when changed to prevent flickering + mainRender(needsRender, threadBuildOptions, currentMenuItemIndex) + needsRender = 0 + + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + menuNavigateDirection += 1 + if key.name == 'KEY_DOWN': + menuNavigateDirection += 1 + if key.name == 'KEY_UP': + menuNavigateDirection -= 1 + if key.name == 'KEY_LEFT': + goBack() + if key.name == 'KEY_ENTER': + runSelection(currentMenuItemIndex) + if key.name == 'KEY_ESCAPE': + return True + elif key: + if key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + mainRender(1, threadBuildOptions, currentMenuItemIndex) + + if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(threadBuildOptions) + needsRender = 2 + + while not isMenuItemSelectable(threadBuildOptions, currentMenuItemIndex): + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(threadBuildOptions) + return True + + + if haltOnErrors: + eval(toRun)() + else: + try: + eval(toRun)() + except: + pass + +# This check isn't required, but placed here for debugging purposes +global currentServiceName # Name of the current service +if currentServiceName == 'otbr': + main() +else: + print("Error. '{}' Tried to run 'otbr' config".format(currentServiceName)) diff --git a/.templates/otbr/select_hardware.py b/.templates/otbr/select_hardware.py new file mode 100755 index 00000000..08d3c49d --- /dev/null +++ b/.templates/otbr/select_hardware.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 + +import signal + +def main(): + from blessed import Terminal + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine + from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName + import time + import subprocess + import ruamel.yaml + import os + + global signal + global currentServiceName + global dockerCommandsSelectionInProgress + global mainMenuList + global currentMenuItemIndex + global renderMode + global paginationSize + global paginationStartIndex + global hardwareFile + global hideHelpText + + global installCommand + + yaml = ruamel.yaml.YAML() + yaml.preserve_quotes = True + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + term = Terminal() + hotzoneLocation = [((term.height // 16) + 6), 0] + paginationToggle = [10, term.height - 25] + paginationStartIndex = 0 + paginationSize = paginationToggle[0] + + serviceService = servicesDirectory + currentServiceName + serviceTemplate = templatesDirectory + currentServiceName + hardwareFileSource = serviceTemplate + '/thread_hardware.yml' + + def goBack(): + global dockerCommandsSelectionInProgress + global needsRender + dockerCommandsSelectionInProgress = False + needsRender = 1 + return True + + mainMenuList = [] + + hotzoneLocation = [((term.height // 16) + 6), 0] + + dockerCommandsSelectionInProgress = True + currentMenuItemIndex = 0 + menuNavigateDirection = 0 + + # Render Modes: + # 0 = No render needed + # 1 = Full render + # 2 = Hotzone only + needsRender = 1 + + def onResize(sig, action): + global mainMenuList + global currentMenuItemIndex + mainRender(1, mainMenuList, currentMenuItemIndex) + + def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): + result = "" + for i in range(paddingBefore): + result += " " + + textPrintableCharactersLength = textLength + + if (textPrintableCharactersLength) == None: + textPrintableCharactersLength = len(text) + + result += text + remainingSpace = lineLength - textPrintableCharactersLength + + for i in range(remainingSpace): + result += " " + + return result + + def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): + global paginationSize + selectedTextLength = len("-> ") + + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + + if paginationStartIndex >= 1: + print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( + b=specialChars[renderMode]["borderVertical"], + uaf=specialChars[renderMode]["upArrowFull"], + ual=specialChars[renderMode]["upArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + for (index, menuItem) in enumerate(menu): # Menu loop + if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: + lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) + + # Menu highlight logic + if index == selection: + formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) + paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) + toPrint = paddedLineText + else: + toPrint = '{title}{t.normal}'.format(t=term, title=lineText) + # ##### + + # Menu check render logic + if menuItem[1]["checked"]: + toPrint = " (X) " + toPrint + else: + toPrint = " ( ) " + toPrint + + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + # ##### + print(toPrint) + + if paginationStartIndex + paginationSize < len(menu): + print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( + b=specialChars[renderMode]["borderVertical"], + daf=specialChars[renderMode]["downArrowFull"], + dal=specialChars[renderMode]["downArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + + + def mainRender(needsRender, menu, selection): + global paginationStartIndex + global paginationSize + term = Terminal() + + if selection >= paginationStartIndex + paginationSize: + paginationStartIndex = selection - (paginationSize - 1) + 1 + needsRender = 1 + + if selection <= paginationStartIndex - 1: + paginationStartIndex = selection + needsRender = 1 + + if needsRender == 1: + print(term.clear()) + print(term.move_y(term.height // 16)) + print(term.black_on_cornsilk4(term.center('IOTstack Thread'))) + print("") + print(term.center(commonTopBorder(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select Thread hardware {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if needsRender >= 1: + renderHotZone(term, needsRender, menu, selection, hotzoneLocation) + + if needsRender == 1: + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + if term.height < 32: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Space] to select or deselect hardware {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to build and save hardware list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + def runSelection(selection): + import types + if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): + mainMenuList[selection][1]() + else: + print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) + + def isMenuItemSelectable(menu, index): + if len(menu) > index: + if len(menu[index]) > 1: + if "skip" in menu[index][1] and menu[index][1]["skip"] == True: + return False + return True + + def loadAddonsMenu(): + global mainMenuList + if os.path.exists(hardwareFileSource): + with open(r'%s' % hardwareFileSource) as objExtrasListFile: + hardwareKnown = yaml.load(objExtrasListFile) + knownExtrasList = hardwareKnown["hardwareList"] + if os.path.exists("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)): + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objSavedExtrasListFile: + savedExtrasList = yaml.load(objSavedExtrasListFile) + savedExtras = [] + + try: + savedExtras = savedExtrasList["hardware"] + except: + print("Error: Loading saved hardware selection. Please resave your selection.") + input("Press Enter to continue...") + + for (index, hardwarePath) in enumerate(knownExtrasList): + if hardwarePath in savedExtras: + mainMenuList.append([hardwarePath, { "checked": True }]) + else: + mainMenuList.append([hardwarePath, { "checked": False }]) + + else: # No saved list + for (index, hardwarePath) in enumerate(knownExtrasList): + if os.path.exists(hardwarePath): + mainMenuList.append([hardwarePath, { "checked": True }]) + else: + mainMenuList.append([hardwarePath, { "checked": False }]) + + + else: + print("Error: '{hardwareFile}' file doesn't exist.".format(hardwareFile=hardwareFileSource)) + input("Press Enter to continue...") + + def checkMenuItem(selection): + global mainMenuList + if mainMenuList[selection][1]["checked"] == True: + mainMenuList[selection][1]["checked"] = False + else: + uncheckAllMenuItems() + mainMenuList[selection][1]["checked"] = True + + def uncheckAllMenuItems(): + global mainMenuList + for menuItem in mainMenuList: + menuItem[1]["checked"] = False + + + def saveAddonList(): + try: + if not os.path.exists(serviceService): + os.makedirs(serviceService, exist_ok=True) + threadYamlExtrasList = { + "version": "1", + "application": "IOTstack", + "service": "otbr", + "comment": "Build Settings", + "hardware": [] + } + for (index, addon) in enumerate(mainMenuList): + if addon[1]["checked"]: + threadYamlExtrasList["hardware"].append(addon[0]) + + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName), 'w') as outputFile: + yaml.dump(threadYamlExtrasList, outputFile) + + except Exception as err: + print("Error saving Thread hardware list", currentServiceName) + print(err) + return False + global hasRebuiltExtrasSelection + hasRebuiltExtrasSelection = True + return True + + + if __name__ == 'builtins': + global signal + term = Terminal() + signal.signal(signal.SIGWINCH, onResize) + loadAddonsMenu() + with term.fullscreen(): + menuNavigateDirection = 0 + mainRender(needsRender, mainMenuList, currentMenuItemIndex) + dockerCommandsSelectionInProgress = True + with term.cbreak(): + while dockerCommandsSelectionInProgress: + menuNavigateDirection = 0 + + if not needsRender == 0: # Only rerender when changed to prevent flickering + mainRender(needsRender, mainMenuList, currentMenuItemIndex) + needsRender = 0 + + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + if paginationSize == paginationToggle[0]: + paginationSize = paginationToggle[1] + else: + paginationSize = paginationToggle[0] + mainRender(1, mainMenuList, currentMenuItemIndex) + if key.name == 'KEY_DOWN': + menuNavigateDirection += 1 + if key.name == 'KEY_UP': + menuNavigateDirection -= 1 + if key.name == 'KEY_ENTER': + if saveAddonList(): + return True + else: + print("Something went wrong. Try saving the list again.") + if key.name == 'KEY_ESCAPE': + dockerCommandsSelectionInProgress = False + return True + elif key: + if key == ' ': # Space pressed + checkMenuItem(currentMenuItemIndex) # Update checked list + needsRender = 2 + elif key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + mainRender(1, mainMenuList, currentMenuItemIndex) + + if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) + needsRender = 2 + + while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) + return True + + return True + +originalSignalHandler = signal.getsignal(signal.SIGINT) +main() +signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/otbr/service.yml b/.templates/otbr/service.yml new file mode 100644 index 00000000..ce32b2de --- /dev/null +++ b/.templates/otbr/service.yml @@ -0,0 +1,23 @@ +otbr: + container_name: otbr + image: openthread/otbr:latest + restart: unless-stopped + # network_mode: host + privileged: true + sysctls: + net.ipv6.conf.all.disable_ipv6: 0 + net.ipv4.conf.all.forwarding: 1 + net.ipv6.conf.all.forwarding: 1 + dns: + - 127.0.0.1 + stdin_open: true + tty: true + volumes: + - otbr-data:/var/lib/otbr + - otbr-wpantund:/etc/wpantund.conf + - otbr-config:/etc/otbr + ports: # For reference only. Thread requires these ports. + - "8283:8283" + command: > + --radio-url spinel+hdlc+uart:///dev/ttyX # Example + diff --git a/.templates/otbr/thread_hardware.yml b/.templates/otbr/thread_hardware.yml new file mode 100644 index 00000000..4a082e7a --- /dev/null +++ b/.templates/otbr/thread_hardware.yml @@ -0,0 +1,11 @@ +version: 1 +application: "IOTstack" +service: "obtr" +comment: "Thread Hardware" +hardwareList: + - "/dev/ttyACM0" + - "/dev/ttyACM1" + - "/dev/ttyACM2" + - "/dev/ttyUSB0" + - "/dev/ttyUSB1" + - "/dev/ttyUSB2" diff --git a/.templates/python-matter-server/build.py b/.templates/python-matter-server/build.py new file mode 100755 index 00000000..ce5ce21a --- /dev/null +++ b/.templates/python-matter-server/build.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 + +issues = {} # Returned issues dict +buildHooks = {} # Options, and others hooks +haltOnErrors = True + +# Main wrapper function. Required to make local vars work correctly +def main(): + import os + import time + import ruamel.yaml + import signal + import sys + from blessed import Terminal + + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine, padText + from deps.common_functions import getExternalPorts, getInternalPorts, checkPortConflicts, enterPortNumberWithWhiptail + from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName, buildCache, servicesFileName + + yaml = ruamel.yaml.YAML() + yaml.preserve_quotes = True + + global dockerComposeServicesYaml # The loaded memory YAML of all checked services + global toRun # Switch for which function to run when executed + global buildHooks # Where to place the options menu result + global currentServiceName # Name of the current service + global issues # Returned issues dict + global haltOnErrors # Turn on to allow erroring + global hideHelpText # Showing and hiding the help controls text + global serviceService + global hasRebuiltExtrasSelection + + serviceService = servicesDirectory + currentServiceName + serviceTemplate = templatesDirectory + currentServiceName + buildSettings = serviceService + buildSettingsFileName + + hasRebuiltExtrasSelection = False + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + documentationHint = 'https://github.com/home-assistant-libs/python-matter-server' + + # runtime vars + portConflicts = [] + + # This lets the menu know whether to put " >> Options " or not + # This function is REQUIRED. + def checkForOptionsHook(): + try: + buildHooks["options"] = callable(runOptionsMenu) + except: + buildHooks["options"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForPreBuildHook(): + try: + buildHooks["preBuildHook"] = callable(preBuild) + except: + buildHooks["preBuildHook"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForPostBuildHook(): + try: + buildHooks["postBuildHook"] = callable(postBuild) + except: + buildHooks["postBuildHook"] = False + return buildHooks + return buildHooks + + # This function is REQUIRED. + def checkForRunChecksHook(): + try: + buildHooks["runChecksHook"] = callable(runChecks) + except: + buildHooks["runChecksHook"] = False + return buildHooks + return buildHooks + + # This service will not check anything unless this is set + # This function is optional, and will run each time the menu is rendered + def runChecks(): + checkForIssues() + return [] + + # This function is optional, and will run after the docker-compose.yml file is written to disk. + def postBuild(): + return True + + # This function is optional, and will run just before the build docker-compose.yml code. + def preBuild(): + global dockerComposeServicesYaml + global currentServiceName + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objExtrasListFile: + pythonMatterServerYamlBuildOptions = yaml.load(objExtrasListFile) + + with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: + serviceYamlTemplate = yaml.load(objServiceFile) + + oldBuildCache = {} + try: + with open(r'%s' % buildCache) as objBuildCache: + oldBuildCache = yaml.load(objBuildCache) + except: + pass + + + buildCacheServices = {} + if "services" in oldBuildCache: + buildCacheServices = oldBuildCache["services"] + + if not os.path.exists(serviceService): + os.makedirs(serviceService, exist_ok=True) + + try: + if currentServiceName in dockerComposeServicesYaml: + if pythonMatterServerYamlBuildOptions["extras"]: + if "Mount Bluetooth: /run/dbus" in pythonMatterServerYamlBuildOptions["extras"]: + if not "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].append("/run/dbus:/run/dbus:ro") + + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if not "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand + "--bluetooth-adapter 0\n" + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + else: + if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") + + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + + if "Enabled Root Certificates" in pythonMatterServerYamlBuildOptions["extras"]: + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if not "--paa-root-cert-dir /data/credentials\n" in currentCommand: + newCommand = currentCommand + "--paa-root-cert-dir /data/credentials\n" + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + else: + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--paa-root-cert-dir /data/credentials\n" in currentCommand: + newCommand = currentCommand.replace("--paa-root-cert-dir /data/credentials\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + else: + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--paa-root-cert-dir /data/credentials\n" in currentCommand: + newCommand = currentCommand.replace("--paa-root-cert-dir /data/credentials\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + + if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") + + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + + + except Exception as err: + print("Error setting pythonMatterServer extras: ", err) + time.sleep(10) + return False + + + + + return True + + # ##################################### + # Supporting functions below + # ##################################### + + def checkForIssues(): + for (index, serviceName) in enumerate(dockerComposeServicesYaml): + if not currentServiceName == serviceName: # Skip self + currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) + portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) + if (len(portConflicts) > 0): + issues["portConflicts"] = portConflicts + + # ##################################### + # End Supporting functions + # ##################################### + + + ############################ + # Menu Logic + ############################ + + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + + selectionInProgress = True + currentMenuItemIndex = 0 + menuNavigateDirection = 0 + needsRender = 1 + term = Terminal() + hotzoneLocation = [((term.height // 16) + 6), 0] + + def goBack(): + global selectionInProgress + global needsRender + selectionInProgress = False + needsRender = 1 + return True + + def selectMatterExtras(): + global needsRender + global hasRebuiltExtrasSelection + matterSelectHardwareFilePath = "./.templates/python-matter-server/select_extras.py" + with open(matterSelectHardwareFilePath, "rb") as pythonDynamicImportFile: + code = compile(pythonDynamicImportFile.read(), matterSelectHardwareFilePath, "exec") + # execGlobals = globals() + # execLocals = locals() + execGlobals = { + "currentServiceName": currentServiceName, + "renderMode": renderMode + } + execLocals = {} + screenActive = False + exec(code, execGlobals, execLocals) + signal.signal(signal.SIGWINCH, onResize) + try: + hasRebuiltExtrasSelection = execGlobals["hasRebuiltExtrasSelection"] + except: + hasRebuiltExtrasSelection = False + screenActive = True + needsRender = 1 + + def onResize(sig, action): + global matterBuildOptions + global currentMenuItemIndex + mainRender(1, matterBuildOptions, currentMenuItemIndex) + + matterBuildOptions = [] + + def createMenu(): + global matterBuildOptions + global serviceService + matterBuildOptions = [] + + + if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): + matterBuildOptions.insert(0, ["Change selected extras", selectMatterExtras]) + else: + matterBuildOptions.insert(0, ["Select extras", selectMatterExtras]) + + matterBuildOptions.append(["Go back", goBack]) + + def runOptionsMenu(): + createMenu() + menuEntryPoint() + return True + + def renderHotZone(term, menu, selection, hotzoneLocation): + lineLengthAtTextStart = 71 + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + for (index, menuItem) in enumerate(menu): + toPrint = "" + if index == selection: + toPrint += ('{bv} -> {t.blue_on_green} {title} {t.normal} <-'.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + else: + toPrint += ('{bv} {t.normal} {title} '.format(t=term, title=menuItem[0], bv=specialChars[renderMode]["borderVertical"])) + + for i in range(lineLengthAtTextStart - len(menuItem[0])): + toPrint += " " + + toPrint += "{bv}".format(bv=specialChars[renderMode]["borderVertical"]) + + toPrint = term.center(toPrint) + + print(toPrint) + + def mainRender(needsRender, menu, selection): + global hasRebuiltExtrasSelection + term = Terminal() + + if needsRender == 1: + print(term.clear()) + print(term.move_y(term.height // 16)) + print(term.black_on_cornsilk4(term.center('IOTstack Python Matter Server Options'))) + print("") + print(term.center(commonTopBorder(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select Option to configure {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if needsRender >= 1: + renderHotZone(term, menu, selection, hotzoneLocation) + + if needsRender == 1: + if os.path.exists("{buildSettings}".format(buildSettings=buildSettings)): + if hasRebuiltExtrasSelection: + print(term.center(commonEmptyLine(renderMode))) + print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Extras list has been rebuilt: build_settings.yml", bv=specialChars[renderMode]["borderVertical"]))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center('{bv} {t.grey_on_blue4} {text} {t.normal}{t.white_on_black}{t.normal} {bv}'.format(t=term, text="Using existing build_settings.yml for hardware installation", bv=specialChars[renderMode]["borderVertical"]))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to run command or save input {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to go back to build stack menu {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + if len(documentationHint) > 1: + if len(documentationHint) > 56: + documentationAndPadding = padText(documentationHint, 71) + print(term.center("{bv} Documentation: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) + else: + documentationAndPadding = padText(documentationHint, 56) + print(term.center("{bv} Documentation: {dap} {bv}".format(bv=specialChars[renderMode]["borderVertical"], dap=documentationAndPadding))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + def runSelection(selection): + import types + global matterBuildOptions + if len(matterBuildOptions[selection]) > 1 and isinstance(matterBuildOptions[selection][1], types.FunctionType): + matterBuildOptions[selection][1]() + else: + print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(nodeRedBuildOptions[selection][0]))) + + def isMenuItemSelectable(menu, index): + if len(menu) > index: + if len(menu[index]) > 2: + if menu[index][2]["skip"] == True: + return False + return True + + def menuEntryPoint(): + # These need to be reglobalised due to eval() + global currentMenuItemIndex + global selectionInProgress + global menuNavigateDirection + global needsRender + global hideHelpText + global matterBuildOptions + term = Terminal() + with term.fullscreen(): + menuNavigateDirection = 0 + mainRender(needsRender, matterBuildOptions, currentMenuItemIndex) + selectionInProgress = True + with term.cbreak(): + while selectionInProgress: + menuNavigateDirection = 0 + + if needsRender: # Only rerender when changed to prevent flickering + mainRender(needsRender, matterBuildOptions, currentMenuItemIndex) + needsRender = 0 + + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + menuNavigateDirection += 1 + if key.name == 'KEY_DOWN': + menuNavigateDirection += 1 + if key.name == 'KEY_UP': + menuNavigateDirection -= 1 + if key.name == 'KEY_LEFT': + goBack() + if key.name == 'KEY_ENTER': + runSelection(currentMenuItemIndex) + if key.name == 'KEY_ESCAPE': + return True + elif key: + if key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + mainRender(1, matterBuildOptions, currentMenuItemIndex) + + if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(matterBuildOptions) + needsRender = 2 + + while not isMenuItemSelectable(matterBuildOptions, currentMenuItemIndex): + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(matterBuildOptions) + return True + + + if haltOnErrors: + eval(toRun)() + else: + try: + eval(toRun)() + except: + pass + +# This check isn't required, but placed here for debugging purposes +global currentServiceName # Name of the current service +if currentServiceName == 'python-matter-server': + main() +else: + print("Error. '{}' Tried to run 'python-matter-server' config".format(currentServiceName)) diff --git a/.templates/python-matter-server/matter_extras.yml b/.templates/python-matter-server/matter_extras.yml new file mode 100644 index 00000000..468ba1cd --- /dev/null +++ b/.templates/python-matter-server/matter_extras.yml @@ -0,0 +1,7 @@ +version: 1 +application: "IOTstack" +service: "python-matter-server" +comment: "Python Matter Server Extras" +extrasList: + - "Mount Bluetooth: /run/dbus" + - "Enabled Root Certificates" diff --git a/.templates/python-matter-server/select_extras.py b/.templates/python-matter-server/select_extras.py new file mode 100755 index 00000000..c07d15c9 --- /dev/null +++ b/.templates/python-matter-server/select_extras.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 + +import signal + +def main(): + from blessed import Terminal + from deps.chars import specialChars, commonTopBorder, commonBottomBorder, commonEmptyLine + from deps.consts import servicesDirectory, templatesDirectory, buildSettingsFileName + import time + import subprocess + import ruamel.yaml + import os + + global signal + global currentServiceName + global dockerCommandsSelectionInProgress + global mainMenuList + global currentMenuItemIndex + global renderMode + global paginationSize + global paginationStartIndex + global extrasFile + global hideHelpText + + global installCommand + + yaml = ruamel.yaml.YAML() + yaml.preserve_quotes = True + + try: # If not already set, then set it. + hideHelpText = hideHelpText + except: + hideHelpText = False + + term = Terminal() + hotzoneLocation = [((term.height // 16) + 6), 0] + paginationToggle = [10, term.height - 25] + paginationStartIndex = 0 + paginationSize = paginationToggle[0] + + serviceService = servicesDirectory + currentServiceName + serviceTemplate = templatesDirectory + currentServiceName + extrasFileSource = serviceTemplate + '/matter_extras.yml' + + def goBack(): + global dockerCommandsSelectionInProgress + global needsRender + dockerCommandsSelectionInProgress = False + needsRender = 1 + return True + + mainMenuList = [] + + hotzoneLocation = [((term.height // 16) + 6), 0] + + dockerCommandsSelectionInProgress = True + currentMenuItemIndex = 0 + menuNavigateDirection = 0 + + # Render Modes: + # 0 = No render needed + # 1 = Full render + # 2 = Hotzone only + needsRender = 1 + + def onResize(sig, action): + global mainMenuList + global currentMenuItemIndex + mainRender(1, mainMenuList, currentMenuItemIndex) + + def generateLineText(text, textLength=None, paddingBefore=0, lineLength=64): + result = "" + for i in range(paddingBefore): + result += " " + + textPrintableCharactersLength = textLength + + if (textPrintableCharactersLength) == None: + textPrintableCharactersLength = len(text) + + result += text + remainingSpace = lineLength - textPrintableCharactersLength + + for i in range(remainingSpace): + result += " " + + return result + + def renderHotZone(term, renderType, menu, selection, hotzoneLocation, paddingBefore = 4): + global paginationSize + selectedTextLength = len("-> ") + + print(term.move(hotzoneLocation[0], hotzoneLocation[1])) + + if paginationStartIndex >= 1: + print(term.center("{b} {uaf} {uaf}{uaf}{uaf} {ual} {b}".format( + b=specialChars[renderMode]["borderVertical"], + uaf=specialChars[renderMode]["upArrowFull"], + ual=specialChars[renderMode]["upArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + + for (index, menuItem) in enumerate(menu): # Menu loop + if index >= paginationStartIndex and index < paginationStartIndex + paginationSize: + lineText = generateLineText(menuItem[0], paddingBefore=paddingBefore) + + # Menu highlight logic + if index == selection: + formattedLineText = '-> {t.blue_on_green}{title}{t.normal} <-'.format(t=term, title=menuItem[0]) + paddedLineText = generateLineText(formattedLineText, textLength=len(menuItem[0]) + selectedTextLength, paddingBefore=paddingBefore - selectedTextLength) + toPrint = paddedLineText + else: + toPrint = '{title}{t.normal}'.format(t=term, title=lineText) + # ##### + + # Menu check render logic + if menuItem[1]["checked"]: + toPrint = " (X) " + toPrint + else: + toPrint = " ( ) " + toPrint + + toPrint = "{bv} {toPrint} {bv}".format(bv=specialChars[renderMode]["borderVertical"], toPrint=toPrint) # Generate border + toPrint = term.center(toPrint) # Center Text (All lines should have the same amount of printable characters) + # ##### + print(toPrint) + + if paginationStartIndex + paginationSize < len(menu): + print(term.center("{b} {daf} {daf}{daf}{daf} {dal} {b}".format( + b=specialChars[renderMode]["borderVertical"], + daf=specialChars[renderMode]["downArrowFull"], + dal=specialChars[renderMode]["downArrowLine"] + ))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + + + def mainRender(needsRender, menu, selection): + global paginationStartIndex + global paginationSize + term = Terminal() + + if selection >= paginationStartIndex + paginationSize: + paginationStartIndex = selection - (paginationSize - 1) + 1 + needsRender = 1 + + if selection <= paginationStartIndex - 1: + paginationStartIndex = selection + needsRender = 1 + + if needsRender == 1: + print(term.clear()) + print(term.move_y(term.height // 16)) + print(term.black_on_cornsilk4(term.center('IOTstack Python Matter Server'))) + print("") + print(term.center(commonTopBorder(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Select Python Matter Server {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + + if needsRender >= 1: + renderHotZone(term, needsRender, menu, selection, hotzoneLocation) + + if needsRender == 1: + print(term.center(commonEmptyLine(renderMode))) + if not hideHelpText: + if term.height < 32: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Not enough vertical room to render controls help text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + else: + print(term.center(commonEmptyLine(renderMode))) + print(term.center("{bv} Controls: {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Space] to select or deselect extras {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Up] and [Down] to move selection cursor {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [H] Show/hide this text {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Enter] to build and save extras list {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center("{bv} [Escape] to cancel changes {bv}".format(bv=specialChars[renderMode]["borderVertical"]))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonEmptyLine(renderMode))) + print(term.center(commonBottomBorder(renderMode))) + + def runSelection(selection): + import types + if len(mainMenuList[selection]) > 1 and isinstance(mainMenuList[selection][1], types.FunctionType): + mainMenuList[selection][1]() + else: + print(term.green_reverse('IOTstack Error: No function assigned to menu item: "{}"'.format(mainMenuList[selection][0]))) + + def isMenuItemSelectable(menu, index): + if len(menu) > index: + if len(menu[index]) > 1: + if "skip" in menu[index][1] and menu[index][1]["skip"] == True: + return False + return True + + def loadAddonsMenu(): + global mainMenuList + if os.path.exists(extrasFileSource): + with open(r'%s' % extrasFileSource) as objExtrasListFile: + extrasKnown = yaml.load(objExtrasListFile) + knownExtrasList = extrasKnown["extrasList"] + if os.path.exists("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)): + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objSavedExtrasListFile: + savedExtrasList = yaml.load(objSavedExtrasListFile) + savedExtras = [] + + try: + savedExtras = savedExtrasList["extras"] + except: + print("Error: Loading saved extras selection. Please resave your selection.") + input("Press Enter to continue...") + + for (index, extrasPath) in enumerate(knownExtrasList): + if extrasPath in savedExtras: + mainMenuList.append([extrasPath, { "checked": True }]) + else: + mainMenuList.append([extrasPath, { "checked": False }]) + + else: # No saved list + for (index, extrasPath) in enumerate(knownExtrasList): + if os.path.exists(extrasPath): + mainMenuList.append([extrasPath, { "checked": True }]) + else: + mainMenuList.append([extrasPath, { "checked": False }]) + + + else: + print("Error: '{extrasFile}' file doesn't exist.".format(extrasFile=extrasFileSource)) + input("Press Enter to continue...") + + def checkMenuItem(selection): + global mainMenuList + if mainMenuList[selection][1]["checked"] == True: + mainMenuList[selection][1]["checked"] = False + else: + mainMenuList[selection][1]["checked"] = True + + def saveAddonList(): + try: + if not os.path.exists(serviceService): + os.makedirs(serviceService, exist_ok=True) + pythonMatterServerYamlExtrasList = { + "version": "1", + "application": "IOTstack", + "service": "pythonMatterServer", + "comment": "Build Settings", + "extras": [] + } + for (index, addon) in enumerate(mainMenuList): + if addon[1]["checked"]: + pythonMatterServerYamlExtrasList["extras"].append(addon[0]) + + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName), 'w') as outputFile: + yaml.dump(pythonMatterServerYamlExtrasList, outputFile) + + except Exception as err: + print("Error saving Python Matter Server list", currentServiceName) + print(err) + return False + global hasRebuiltExtrasSelection + hasRebuiltExtrasSelection = True + return True + + + if __name__ == 'builtins': + global signal + term = Terminal() + signal.signal(signal.SIGWINCH, onResize) + loadAddonsMenu() + with term.fullscreen(): + menuNavigateDirection = 0 + mainRender(needsRender, mainMenuList, currentMenuItemIndex) + dockerCommandsSelectionInProgress = True + with term.cbreak(): + while dockerCommandsSelectionInProgress: + menuNavigateDirection = 0 + + if not needsRender == 0: # Only rerender when changed to prevent flickering + mainRender(needsRender, mainMenuList, currentMenuItemIndex) + needsRender = 0 + + key = term.inkey(esc_delay=0.05) + if key.is_sequence: + if key.name == 'KEY_TAB': + if paginationSize == paginationToggle[0]: + paginationSize = paginationToggle[1] + else: + paginationSize = paginationToggle[0] + mainRender(1, mainMenuList, currentMenuItemIndex) + if key.name == 'KEY_DOWN': + menuNavigateDirection += 1 + if key.name == 'KEY_UP': + menuNavigateDirection -= 1 + if key.name == 'KEY_ENTER': + if saveAddonList(): + return True + else: + print("Something went wrong. Try saving the list again.") + if key.name == 'KEY_ESCAPE': + dockerCommandsSelectionInProgress = False + return True + elif key: + if key == ' ': # Space pressed + checkMenuItem(currentMenuItemIndex) # Update checked list + needsRender = 2 + elif key == 'h': # H pressed + if hideHelpText: + hideHelpText = False + else: + hideHelpText = True + mainRender(1, mainMenuList, currentMenuItemIndex) + + if menuNavigateDirection != 0: # If a direction was pressed, find next selectable item + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) + needsRender = 2 + + while not isMenuItemSelectable(mainMenuList, currentMenuItemIndex): + currentMenuItemIndex += menuNavigateDirection + currentMenuItemIndex = currentMenuItemIndex % len(mainMenuList) + return True + + return True + +originalSignalHandler = signal.getsignal(signal.SIGINT) +main() +signal.signal(signal.SIGWINCH, originalSignalHandler) diff --git a/.templates/python-matter-server/service.yml b/.templates/python-matter-server/service.yml new file mode 100644 index 00000000..de71b6a4 --- /dev/null +++ b/.templates/python-matter-server/service.yml @@ -0,0 +1,15 @@ +python-matter-server: + container_name: python-matter-server + image: ghcr.io/home-assistant-libs/python-matter-server:stable + restart: unless-stopped + network_mode: host + security_opt: + - apparmor=unconfined + volumes: + - ./volumes/python-matter-server/data:/data + ports: # For reference only. Matter requires these ports. + - "5580:5580" + - "5080:5080" + command: > + --storage-path /data + From 313faf2e26c850b558541274936404c69c0afe47 Mon Sep 17 00:00:00 2001 From: Slyke Date: Fri, 8 Nov 2024 03:31:20 -0800 Subject: [PATCH 2/5] Added matterbridge --- .templates/matterbridge/service.yml | 9 +++++++++ .templates/otbr/service.yml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .templates/matterbridge/service.yml diff --git a/.templates/matterbridge/service.yml b/.templates/matterbridge/service.yml new file mode 100644 index 00000000..20e07f07 --- /dev/null +++ b/.templates/matterbridge/service.yml @@ -0,0 +1,9 @@ +matterbridge: + container_name: matterbridge + image: "luligu/matterbridge:1.6.1" + restart: unless-stopped + ports: + - "8284:8283" + volumes: + - ./volumes/matterbridge/data:/root/.matterbridge + - ./volumes/matterbridge/plugins:/root/Matterbridge diff --git a/.templates/otbr/service.yml b/.templates/otbr/service.yml index ce32b2de..3e78a87c 100644 --- a/.templates/otbr/service.yml +++ b/.templates/otbr/service.yml @@ -16,8 +16,8 @@ otbr: - otbr-data:/var/lib/otbr - otbr-wpantund:/etc/wpantund.conf - otbr-config:/etc/otbr - ports: # For reference only. Thread requires these ports. - - "8283:8283" + ports: + - "80:8283" command: > --radio-url spinel+hdlc+uart:///dev/ttyX # Example From 18c0ccb4d779ce8710c9a882ed65860cd1dcabec Mon Sep 17 00:00:00 2001 From: Slyke Date: Fri, 8 Nov 2024 03:54:53 -0800 Subject: [PATCH 3/5] Fixed bug where menu would crash when no build options selected --- .templates/otbr/build.py | 24 ++--- .templates/otbr/service.yml | 6 +- .templates/python-matter-server/build.py | 119 ++++++++++++----------- 3 files changed, 76 insertions(+), 73 deletions(-) diff --git a/.templates/otbr/build.py b/.templates/otbr/build.py index bb15b805..cecdf58c 100755 --- a/.templates/otbr/build.py +++ b/.templates/otbr/build.py @@ -140,23 +140,25 @@ def preBuild(): # ##################################### def checkForIssues(): - with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objHardwareListFile: - otbrYamlBuildOptions = yaml.load(objHardwareListFile) + passed = True + try: + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objHardwareListFile: + otbrYamlBuildOptions = yaml.load(objHardwareListFile) + if not otbrYamlBuildOptions["hardware"] or len(otbrYamlBuildOptions["hardware"]) < 1: + issues["hardware"] = "No Thread radio selected." + passed = False + if otbrYamlBuildOptions["hardware"] and len(otbrYamlBuildOptions["hardware"]) > 1: + issues["hardware"] = "Two or more thread radios selected. The first listed one will be used" + passed = False + except Exception as err: + issues["hardware"] = "No Thread radio selected." for (index, serviceName) in enumerate(dockerComposeServicesYaml): if not currentServiceName == serviceName: # Skip self currentServicePorts = getExternalPorts(currentServiceName, dockerComposeServicesYaml) portConflicts = checkPortConflicts(serviceName, currentServicePorts, dockerComposeServicesYaml) if (len(portConflicts) > 0): issues["portConflicts"] = portConflicts - - print(otbrYamlBuildOptions["hardware"]) - if not otbrYamlBuildOptions["hardware"] or len(otbrYamlBuildOptions["hardware"]) < 1: - issues["hardware"] = "No Thread radio selected." - if otbrYamlBuildOptions["hardware"] and len(otbrYamlBuildOptions["hardware"]) > 1: - issues["hardware"] = "Two or more thread radios selected. The first listed one will be used" - - - + passed = False # ##################################### # End Supporting functions diff --git a/.templates/otbr/service.yml b/.templates/otbr/service.yml index 3e78a87c..05fe18a6 100644 --- a/.templates/otbr/service.yml +++ b/.templates/otbr/service.yml @@ -13,9 +13,9 @@ otbr: stdin_open: true tty: true volumes: - - otbr-data:/var/lib/otbr - - otbr-wpantund:/etc/wpantund.conf - - otbr-config:/etc/otbr + - ./volumes/otbr/data:/var/lib/otbr + - ./volumes/otbr/wpantund:/etc/wpantund.conf + - ./volumes/otbr/config:/etc/otbr ports: - "80:8283" command: > diff --git a/.templates/python-matter-server/build.py b/.templates/python-matter-server/build.py index ce5ce21a..261aceb8 100755 --- a/.templates/python-matter-server/build.py +++ b/.templates/python-matter-server/build.py @@ -97,76 +97,77 @@ def postBuild(): def preBuild(): global dockerComposeServicesYaml global currentServiceName - with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objExtrasListFile: - pythonMatterServerYamlBuildOptions = yaml.load(objExtrasListFile) - - with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: - serviceYamlTemplate = yaml.load(objServiceFile) - - oldBuildCache = {} try: - with open(r'%s' % buildCache) as objBuildCache: - oldBuildCache = yaml.load(objBuildCache) - except: - pass - + with open("{serviceDir}{buildSettings}".format(serviceDir=serviceService, buildSettings=buildSettingsFileName)) as objExtrasListFile: + pythonMatterServerYamlBuildOptions = yaml.load(objExtrasListFile) + + with open((r'%s/' % serviceTemplate) + servicesFileName) as objServiceFile: + serviceYamlTemplate = yaml.load(objServiceFile) + + oldBuildCache = {} + try: + with open(r'%s' % buildCache) as objBuildCache: + oldBuildCache = yaml.load(objBuildCache) + except: + pass - buildCacheServices = {} - if "services" in oldBuildCache: - buildCacheServices = oldBuildCache["services"] - - if not os.path.exists(serviceService): - os.makedirs(serviceService, exist_ok=True) - - try: - if currentServiceName in dockerComposeServicesYaml: - if pythonMatterServerYamlBuildOptions["extras"]: - if "Mount Bluetooth: /run/dbus" in pythonMatterServerYamlBuildOptions["extras"]: - if not "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: - dockerComposeServicesYaml[currentServiceName]["volumes"].append("/run/dbus:/run/dbus:ro") - - currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] - if not "--bluetooth-adapter 0\n" in currentCommand: - newCommand = currentCommand + "--bluetooth-adapter 0\n" - dockerComposeServicesYaml[currentServiceName]["command"] = newCommand - else: - if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: - dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") - - currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] - if "--bluetooth-adapter 0\n" in currentCommand: - newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") + buildCacheServices = {} + if "services" in oldBuildCache: + buildCacheServices = oldBuildCache["services"] + + if not os.path.exists(serviceService): + os.makedirs(serviceService, exist_ok=True) + + try: + if currentServiceName in dockerComposeServicesYaml: + if pythonMatterServerYamlBuildOptions["extras"]: + if "Mount Bluetooth: /run/dbus" in pythonMatterServerYamlBuildOptions["extras"]: + if not "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].append("/run/dbus:/run/dbus:ro") + + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if not "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand + "--bluetooth-adapter 0\n" dockerComposeServicesYaml[currentServiceName]["command"] = newCommand - - if "Enabled Root Certificates" in pythonMatterServerYamlBuildOptions["extras"]: - currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] - if not "--paa-root-cert-dir /data/credentials\n" in currentCommand: - newCommand = currentCommand + "--paa-root-cert-dir /data/credentials\n" - dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + else: + if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") + + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + + if "Enabled Root Certificates" in pythonMatterServerYamlBuildOptions["extras"]: + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if not "--paa-root-cert-dir /data/credentials\n" in currentCommand: + newCommand = currentCommand + "--paa-root-cert-dir /data/credentials\n" + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + else: + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--paa-root-cert-dir /data/credentials\n" in currentCommand: + newCommand = currentCommand.replace("--paa-root-cert-dir /data/credentials\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand else: currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] if "--paa-root-cert-dir /data/credentials\n" in currentCommand: newCommand = currentCommand.replace("--paa-root-cert-dir /data/credentials\n", "") dockerComposeServicesYaml[currentServiceName]["command"] = newCommand - else: - currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] - if "--paa-root-cert-dir /data/credentials\n" in currentCommand: - newCommand = currentCommand.replace("--paa-root-cert-dir /data/credentials\n", "") - dockerComposeServicesYaml[currentServiceName]["command"] = newCommand - if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: - dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") - - currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] - if "--bluetooth-adapter 0\n" in currentCommand: - newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") - dockerComposeServicesYaml[currentServiceName]["command"] = newCommand + if "/run/dbus:/run/dbus:ro" in dockerComposeServicesYaml[currentServiceName]["volumes"]: + dockerComposeServicesYaml[currentServiceName]["volumes"].remove("/run/dbus:/run/dbus:ro") + currentCommand = dockerComposeServicesYaml[currentServiceName]["command"] + if "--bluetooth-adapter 0\n" in currentCommand: + newCommand = currentCommand.replace("--bluetooth-adapter 0\n", "") + dockerComposeServicesYaml[currentServiceName]["command"] = newCommand - except Exception as err: - print("Error setting pythonMatterServer extras: ", err) - time.sleep(10) - return False + except Exception as err: + print("Error setting pythonMatterServer extras: ", err) + time.sleep(10) + return False + except: + pass From babc03a185536d6b5df52aeb6c1823759b32c3bb Mon Sep 17 00:00:00 2001 From: Slyke Date: Fri, 8 Nov 2024 04:43:22 -0800 Subject: [PATCH 4/5] Added basic readme for nrf52840 for Thread --- .templates/otbr/readme.md | 68 +++++++++++++++++++++ .templates/otbr/service.yml | 2 +- .templates/python-matter-server/service.yml | 6 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 .templates/otbr/readme.md diff --git a/.templates/otbr/readme.md b/.templates/otbr/readme.md new file mode 100644 index 00000000..c66443e1 --- /dev/null +++ b/.templates/otbr/readme.md @@ -0,0 +1,68 @@ +# OTBR (Open Thread Border Router) + +The container will fail to show the WUI until IPv6 is enabled on the RPi. You can do so by running the following commands: +``` +$ sudo modprobe ip6_tables +$ sudo modprobe ip6table_filter +``` + +Save between reboots: +``` +$ echo "ip6_tables" | sudo tee -a /etc/modules +$ echo "ip6table_filter" | sudo tee -a /etc/modules +``` + +Open docker config `sudo nano /etc/docker/daemon.json`: +``` +{ + "ipv6": true, + "fixed-cidr-v6": "2001:db8:1::/64" +} +``` + +Then: +``` +$ sudo systemctl restart docker +``` + +I have this successfully working with a MakerDiary nrf52840 USB Thread radio node. It requires custom firmware flashed on it. + +You can flash the USB card with the `openthread/environment:latest` docker image. You only need to flash the firmware once to the USB radio, it has ran on every device I've tested running OTBR: baremetal, Docker, IOTstack, and Kubernetes (containerd). + +Run the following commands in the `openthread/environment` docker instance: +``` +$ git clone https://github.com/openthread/ot-nrf528xx.git +$ cd ot-nrf528xx/ +$ git submodule update --init +$ ./script/build nrf52840 USB_trans -DOT_BOOTLOADER=USB +``` + +After this, it depends on the type of nRF52840 you're using. If you are using a MakerDiary, mount it as a drive and drag the UF2 file into it, after converting it to a .hex file, and then a UF2 file: +``` +$ arm-none-eabi-objcopy -O ihex build/bin/ot-cli-ftd ot-cli-ftd.hex +$ pip install --pre -U git+https://github.com/makerdiary/uf2utils.git@main +$ uf2conv -f 0xADA52840 -c -b 0x1000 -o build/bin/ot-cli-ftd.uf2 build/bin/ot-cli-ftd +``` + +Since I run Zigbee and zwave on the same device, I mounted the nRF52840 this way `compose-override.yml`: +``` +services: + otbr: + volumes: + - ./volumes/otbr/data:/var/lib/otbr + - ./volumes/otbr/wpantund:/etc/wpantund.conf + - ./volumes/otbr/config:/etc/otbr + - /dev/serial/by-id/usb-Nordic_Semiconductor_nRF528xx_OpenThread_Device_XXXXXXXXXXX-if00:/dev/ttyACM0 +``` + +Note the device serial number has been replaced with Xs. You can find yours by running: +``` +ls -ahl /dev +``` + +You need to have flashed it with the OTBR firmware before running this command, as it will have a different name if running the stock firmware. + +Links: +* https://openthread.io/guides/border-router/docker (OTBR running in docker) +* https://openthread.io/guides/build/index.md (Radio/Node/RCP binary compile and firmware flashing) +* https://openthread.io/guides/border-router/raspberry-pi (Running on RPi 3+ bare-metal) \ No newline at end of file diff --git a/.templates/otbr/service.yml b/.templates/otbr/service.yml index 05fe18a6..a0ada6e6 100644 --- a/.templates/otbr/service.yml +++ b/.templates/otbr/service.yml @@ -17,7 +17,7 @@ otbr: - ./volumes/otbr/wpantund:/etc/wpantund.conf - ./volumes/otbr/config:/etc/otbr ports: - - "80:8283" + - "8283:80" command: > --radio-url spinel+hdlc+uart:///dev/ttyX # Example diff --git a/.templates/python-matter-server/service.yml b/.templates/python-matter-server/service.yml index de71b6a4..57696595 100644 --- a/.templates/python-matter-server/service.yml +++ b/.templates/python-matter-server/service.yml @@ -7,9 +7,9 @@ python-matter-server: - apparmor=unconfined volumes: - ./volumes/python-matter-server/data:/data - ports: # For reference only. Matter requires these ports. - - "5580:5580" - - "5080:5080" + # ports: # For reference only. Matter requires these ports. + # - "5580:5580" + # - "5080:5080" command: > --storage-path /data From a1e443fd1b8ba819ead7982066fe917da471171c Mon Sep 17 00:00:00 2001 From: Slyke Date: Wed, 18 Dec 2024 16:41:59 -0800 Subject: [PATCH 5/5] Updated matterbridge network mode to host --- .templates/matterbridge/service.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.templates/matterbridge/service.yml b/.templates/matterbridge/service.yml index 20e07f07..98fa8406 100644 --- a/.templates/matterbridge/service.yml +++ b/.templates/matterbridge/service.yml @@ -1,9 +1,8 @@ matterbridge: container_name: matterbridge - image: "luligu/matterbridge:1.6.1" + image: "luligu/matterbridge:1.6.7" restart: unless-stopped - ports: - - "8284:8283" + network_mode: host volumes: - ./volumes/matterbridge/data:/root/.matterbridge - ./volumes/matterbridge/plugins:/root/Matterbridge