Add version and hash to docker container (#167)

* Add version and git hash to image
* Add version reporting to HA via api
This commit is contained in:
Florian Asche authored and GitHub committed 2026-02-21 20:38:46 +01:00
1 parent 76308cc209
commit 7de510cb1f
13 files changed
+123 -12

No files matched your search

+2
View File
@@ -8,3 +8,5 @@
!/sounds/* !/sounds/*
!/wakewords/* !/wakewords/*
!/wakewords/openWakeWord/* !/wakewords/openWakeWord/*
!/version.txt
!/version_githash.txt
@@ -44,6 +44,12 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Create version files
run: |
echo "${{ env.CLEAN_BRANCH_NAME }}" > version.txt
echo "${{ github.sha }}" > version_githash.txt
ls -ltr
- name: Build and push Docker image - name: Build and push Docker image
id: push id: push
uses: docker/build-push-action@v5.3.0 uses: docker/build-push-action@v5.3.0
@@ -25,6 +25,7 @@ jobs:
run: | run: |
echo "REGISTRY_LOWER=$(echo '${{ env.REGISTRY }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "REGISTRY_LOWER=$(echo '${{ env.REGISTRY }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV echo "REPOSITORY_LOWER=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
@@ -36,6 +37,13 @@ jobs:
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Create version files
run: |
echo "nightly" > version.txt
echo "${{ github.sha }}" > version_githash.txt
ls -ltr
- name: Build and push Docker image - name: Build and push Docker image
id: push id: push
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
@@ -45,6 +45,13 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
- name: Create version files
run: |
echo "${{ steps.meta.outputs.version }}" > version.txt
echo "${{ github.sha }}" > version_githash.txt
ls -ltr
- name: Build and push Docker image - name: Build and push Docker image
id: push id: push
uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
+2
View File
@@ -21,3 +21,5 @@ venv/
# Docker # Docker
.env .env
version.txt
version_githash.txt
+2
View File
@@ -54,6 +54,8 @@ COPY sounds/ ./sounds/
COPY ../wakewords/ ./wakewords/ COPY ../wakewords/ ./wakewords/
COPY linux_voice_assistant/ ./linux_voice_assistant/ COPY linux_voice_assistant/ ./linux_voice_assistant/
COPY docker-entrypoint.sh ./ COPY docker-entrypoint.sh ./
COPY version.txt ./
COPY version_githash.txt ./
### Run installation: ### Run installation:
RUN chmod +x docker-entrypoint.sh RUN chmod +x docker-entrypoint.sh
+2
View File
@@ -47,6 +47,8 @@ services:
- ${LVA_XDG_RUNTIME_DIR}:${LVA_XDG_RUNTIME_DIR} - ${LVA_XDG_RUNTIME_DIR}:${LVA_XDG_RUNTIME_DIR}
# Mount source code for development # Mount source code for development
- ./linux_voice_assistant:/app/linux_voice_assistant - ./linux_voice_assistant:/app/linux_voice_assistant
- ./version.txt:/app/version.txt
- ./version_githash.txt:/app/version_githash.txt
depends_on: depends_on:
- fix-permissions - fix-permissions
# Health check for process # Health check for process
+23 -6
View File
@@ -22,6 +22,8 @@ from getmac import get_mac_address
from .zeroconf import HomeAssistantZeroconf from .zeroconf import HomeAssistantZeroconf
from .util import get_default_interface from .util import get_default_interface
from .util import get_default_ipv4 from .util import get_default_ipv4
from .util import get_version
from .util import get_esphome_version
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_MODULE_DIR = Path(__file__).parent _MODULE_DIR = Path(__file__).parent
@@ -183,13 +185,25 @@ async def main() -> None:
# Resolve name # Resolve name
if not args.name: if not args.name:
print("No name specified, try to autogenerate name") print("No friendly name specified, try to autogenerate name")
device_name = f"lva-{mac_address_clean}" friendly_name = f"LVA - {mac_address_clean}"
print(f"Name autogenerated: {device_name}") print(f"Friendly name autogenerated: {friendly_name}")
else: else:
print("Name specified") print("Friendly name specified")
print(f"Using name: {args.name}") print(f"Using friendly name: {args.name}")
device_name = args.name friendly_name = args.name
mac_no_colon = mac_address.replace(":", "").lower()
device_name = f"lva-{mac_no_colon}"
print(f"Device name: {device_name}")
# Resolve version
version = get_version()
print(f"Version: {version}")
# Resolve esphome version
esphome_version = get_esphome_version()
print(f"ESPHome api version: {esphome_version}")
# Resolve download dir # Resolve download dir
args.download_dir = Path(args.download_dir) args.download_dir = Path(args.download_dir)
@@ -288,9 +302,12 @@ async def main() -> None:
state = ServerState( state = ServerState(
name=device_name, name=device_name,
friendly_name=friendly_name,
network_interface=network_interface, network_interface=network_interface,
mac_address=get_mac_address(interface=network_interface), mac_address=get_mac_address(interface=network_interface),
ip_address=host_ip_address, ip_address=host_ip_address,
version=version,
esphome_version=esphome_version,
audio_queue=Queue(), audio_queue=Queue(),
entities=[], entities=[],
available_wake_words=available_wake_words, available_wake_words=available_wake_words,
+3
View File
@@ -63,9 +63,12 @@ class Preferences:
@dataclass @dataclass
class ServerState: class ServerState:
name: str name: str
friendly_name: str
mac_address: str mac_address: str
ip_address: str ip_address: str
network_interface: str network_interface: str
version: str
esphome_version: str
audio_queue: "Queue[Optional[bytes]]" audio_queue: "Queue[Optional[bytes]]"
entities: "List[ESPHomeEntity]" entities: "List[ESPHomeEntity]"
available_wake_words: "Dict[str, AvailableWakeWord]" available_wake_words: "Dict[str, AvailableWakeWord]"
+6 -6
View File
@@ -291,15 +291,15 @@ class VoiceSatelliteProtocol(APIServer):
elif isinstance(msg, VoiceAssistantTimerEventResponse): elif isinstance(msg, VoiceAssistantTimerEventResponse):
self.handle_timer_event(VoiceAssistantTimerEventType(msg.event_type), msg) self.handle_timer_event(VoiceAssistantTimerEventType(msg.event_type), msg)
elif isinstance(msg, DeviceInfoRequest): elif isinstance(msg, DeviceInfoRequest):
# Compute dynamic device name _LOGGER.debug("Device info request")
base_name = re.sub(r"[\s-]+", "-", self.state.name.lower()).strip("-")
mac_no_colon = self.state.mac_address.replace(":", "").lower()
mac_last6 = mac_no_colon[-6:]
device_name = f"{base_name}-{mac_last6}"
yield DeviceInfoResponse( yield DeviceInfoResponse(
uses_password=False, uses_password=False,
name=device_name, name=self.state.name,
friendly_name=self.state.friendly_name,
project_name="Open Home Foundation.Linux Voice Assistant",
project_version=self.state.version,
esphome_version=self.state.esphome_version,
mac_address=self.state.mac_address, mac_address=self.state.mac_address,
manufacturer="Open Home Foundation", manufacturer="Open Home Foundation",
model="Linux Voice Assistant", model="Linux Voice Assistant",
+60
View File
@@ -4,8 +4,68 @@ import uuid
# netifaces lib is from netifaces2 # netifaces lib is from netifaces2
import netifaces import netifaces
from collections.abc import Callable from collections.abc import Callable
from importlib.metadata import version, PackageNotFoundError
from pathlib import Path
from typing import Optional from typing import Optional
# Cache for version to avoid repeated file reading
_version_cache: Optional[str] = None
_esphome_version_cache: Optional[str] = None
def get_version() -> str:
"""
Read the version from version.txt file.
This function reads the content safely without risk of code injection,
as it only reads raw text and performs no evaluation.
Returns:
str: The version from version.txt or 'unknown' if the file
does not exist or cannot be read.
"""
global _version_cache
if _version_cache is not None:
return _version_cache
version_file = Path(__file__).parent.parent / "version.txt"
try:
# Sicher lesen: nur Rohtext, keine Evaluierung
version = version_file.read_text(encoding="utf-8").strip()
_version_cache = version if version else "unknown"
except (FileNotFoundError, PermissionError, OSError):
_version_cache = "unknown"
return _version_cache
def get_esphome_version() -> str:
"""
Read the version of the installed aioesphomeapi package.
This function uses importlib.metadata to safely retrieve the version
of an installed Python package without executing any code from the
package itself.
Returns:
str: The version of aioesphomeapi (e.g., '42.7.0'), or 'unknown'
if the package is not installed or the version cannot be read.
"""
global _esphome_version_cache
if _esphome_version_cache is not None:
return _esphome_version_cache
try:
_esphome_version_cache = version("aioesphomeapi")
except PackageNotFoundError:
_esphome_version_cache = "unknown"
return _esphome_version_cache
def call_all(*callables: Optional[Callable[[], None]]) -> None: def call_all(*callables: Optional[Callable[[], None]]) -> None:
for item in filter(None, callables): for item in filter(None, callables):
item() item()
+1
View File
@@ -0,0 +1 @@
placeholder
+1
View File
@@ -0,0 +1 @@
placeholder