Skip to content

Commit

Permalink
2024-07-21 Adds ESPHome - master branch - PR 1 of 2
Browse files Browse the repository at this point in the history
Adds ESPHome container, as requested by #754.

Includes documentation.

Fixes: #754

Signed-off-by: Phill Kelley <[email protected]>
  • Loading branch information
Paraphraser committed Jul 21, 2024
1 parent b1d67ed commit a890000
Show file tree
Hide file tree
Showing 15 changed files with 532 additions and 0 deletions.
47 changes: 47 additions & 0 deletions .templates/esphome/88-tty-iotstack-esphome.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Assumptions:
#
# 1. The ESPhome container is running with the container-name "esphome".
#
# 2. The service definition for the ESPhome container includes:
#
# device_cgroup_rules:
# - 'c 188:* rw'
#
# This clause permits the container to access any device with a major
# number 188, which captures most USB-to-serial adapters that are
# found on ESP32 dev boards or equivalent off-board adapters such as
# those made by Future Technology Devices International (FTDI) and
# Silicon Laboratories Incorporated. The major number 188 also shows
# up in the UDEV rules below.
#
# 3. The ESP device to be managed is mounted and/or unmounted WHILE the
# container is running. In other words, all bets are off if the host
# system reboots or the container starts while the USB device is
# connected. You will likely need to unplug/replug the device to
# get the container back in sync.
#
# The rules do NOT check if the container is running and do NOT check
# for errors. All that will happen is errors in the system log.
#
# Removing ESPhome from your stack does NOT remove this rules file. It
# does not matter whether you accomplish removal by editing your compose
# file or via the IOTstack menu, this rule will be left in place and it
# will generate an error every time it fires in response to insertion
# or removal of a matching USB device.
#
# It is perfectly safe to remove this rules file yourself:
#
# sudo rm /etc/udev/rules.d/88-tty-iotstack-esphome.rules
#
# That's all you have to do. UDEV is dynamic and, despite what you read
# on the web, does NOT have to be restarted or reloaded.

# Upon insertion of a matching USB device, mount the same device inside the container
ACTION=="add", \
SUBSYSTEM=="tty", ENV{MAJOR}=="188", \
RUN+="/usr/bin/docker exec esphome mknod %E{DEVNAME} c %M %m"

# Upon removal of a matching USB device, remove the same device inside the container
ACTION=="remove", \
SUBSYSTEM=="tty", ENV{MAJOR}=="188", \
RUN+="/usr/bin/docker exec esphome rm -f %E{DEVNAME}"
175 changes: 175 additions & 0 deletions .templates/esphome/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

issues = {} # Returned issues dict
buildHooks = {} # Options, and others hooks
haltOnErrors = True

import os
import sys

global templatesDirectory
global currentServiceName # Name of the current service
global generateRandomString

from deps.consts import templatesDirectory
from deps.common_functions import generateRandomString


# Main wrapper function. Required to make local vars work correctly
def main():

global toRun # Switch for which function to run when executed
global buildHooks # Where to place the options menu result
global issues # Returned issues dict
global haltOnErrors # Turn on to allow erroring

# 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():
return True

# #####################################
# Supporting functions below
# #####################################

def doCustomSetup() :

import os
import re
import subprocess
from os.path import exists

def copyUdevRulesFile(templates,rules) :

# the expected location of the rules file in the template is the absolute path ...
SOURCE_PATH = templates + '/' + currentServiceName + '/' + rules

# the rules file should be installed at the following absolute path...
TARGET_PATH = '/etc/udev/rules.d' + '/' + rules

# does the target already exist?
if not exists(TARGET_PATH) :

# no! does the source path exist?
if exists(SOURCE_PATH) :

# yes! we should copy the source to the target
subprocess.call(['sudo', 'cp', SOURCE_PATH, TARGET_PATH])

# sudo cp sets root ownership but not necessarily correct mode
subprocess.call(['sudo', 'chmod', '644', TARGET_PATH])

def setEnvironment (path, key, value) :

# assume the variable should be written
shouldWrite = True

# does the target file already exist?
if exists(path) :

# yes! open the file so we can search it
env_file = open(path, 'r+')

# prepare to read by lines
env_data = env_file.readlines()

# we are searching for...
expression = '^' + key + '='

# search by line
for line in env_data:
if re.search(expression, line) :
shouldWrite = False
break

else :

# no! create the file
env_file = open(path, 'w')

# should the variable be written?
if shouldWrite :
print(key + '=' + value, file=env_file)

# done with the environment file
env_file.close()

copyUdevRulesFile(
os.path.realpath(templatesDirectory),
'88-tty-iotstack-' + currentServiceName + '.rules'
)

# the environment file is located at ...
DOT_ENV_PATH = os.path.realpath('.') + '/.env'

# check/set environment variables
setEnvironment(DOT_ENV_PATH,'ESPHOME_USERNAME',currentServiceName)
setEnvironment(DOT_ENV_PATH,'ESPHOME_PASSWORD',generateRandomString())


def checkForIssues():
doCustomSetup() # done here because is called least-frequently
return True

if haltOnErrors:
eval(toRun)()
else:
try:
eval(toRun)()
except:
pass

if currentServiceName == 'esphome':
main()
else:
print("Error. '{}' Tried to run 'plex' config".format(currentServiceName))
15 changes: 15 additions & 0 deletions .templates/esphome/service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
esphome:
container_name: esphome
image: esphome/esphome
restart: unless-stopped
environment:
- TZ=${TZ:-Etc/UTC}
- USERNAME=${ESPHOME_USERNAME:-esphome}
- PASSWORD=${ESPHOME_PASSWORD:?eg echo ESPHOME_PASSWORD=ChangeMe >>~/IOTstack/.env}
network_mode: host
x-ports:
- "6052:6052"
volumes:
- ./volumes/esphome/config:/config
device_cgroup_rules:
- 'c 188:* rw'
Loading

0 comments on commit a890000

Please sign in to comment.