hotfix for mac-detection and various variable names (#139)

* Change Client-Name: The client name inside LVA is not capable of having spaces in the name. Fixed the example.
* Add first version of Docker-Compose for developers
* Add variable for network-interface and auto detection by default
* Added autodetection for the MAC-Address based on the detected or specified network-interface.
* Switched from listening on all IP-Addresses to the IP of the default interface, usually u set the correct network-interface and it will automatically detect the right IP-Address.
* Added missing variables, fixed variables names
* Fixed not working variables for example sound-files.
* Moved the auto-name creation from shell to python. The name is no longer mandatory, it will be generated if no name is given.
This commit is contained in:
Florian Asche authored and GitHub committed 2026-02-13 19:50:20 +01:00
1 parent f72e5c8d30
commit 4052daae01
11 files changed
+256 -113

No files matched your search

+11 -3
View File
@@ -14,8 +14,7 @@ LVA_USER_ID="1000"
LVA_USER_GROUP="1000"
### Name for the client (optional):
# by default it uses the HOSTNAME variable from the piCompose environment which includes the MAC from the network card
# CLIENT_NAME="My Voice Assistant Speaker"
# CLIENT_NAME="speaker_livvingroom"
### PulseAudio socket path on the host:
# PulseAudio Server: /run/user/1000/pulse
@@ -26,6 +25,12 @@ LVA_XDG_RUNTIME_DIR="/run/user/${LVA_USER_ID}"
### Path to the preferences file (optional):
# PREFERENCES_FILE="/app/configuration/preferences.json"
### Network interface (optional):
# NETWORK_INTERFACE="eth0"
### IP-Address for the api (optional):
# HOST="0.0.0.0"
### Port for the api (optional):
# PORT="6053"
@@ -39,7 +44,10 @@ LVA_XDG_RUNTIME_DIR="/run/user/${LVA_USER_ID}"
# ENABLE_THINKING_SOUND="1"
# Wake model (optional):
# WAKE-MODEL="okay_nabu"
# WAKE_MODEL="okay_nabu"
# Stop model (optional):
# STOP_MODEL="stop"
# Refactory seconds (optional):
# REFACTORY_SECONDS="2"
+1
View File
@@ -39,6 +39,7 @@ apt-get install --yes --no-install-recommends \
libasound2-plugins \
ca-certificates \
iproute2 \
vim \
procps && \
apt-get clean
+3 -2
View File
@@ -47,7 +47,7 @@ usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE]
| Parameter | Description | Default |
|-----------|-------------|---------|
| `--name` | Name of the voice assistant device (required) | - |
| `--name` | Name of the voice assistant device (required) | Autogenerated (`lva-MAC-ADDRESS`) |
| `--audio-input-device` | Soundcard name for input device | System default microphone |
| `--audio-input-block-size` | Audio input block size in samples | 1024 |
| `--audio-output-device` | mpv name for output device | System default speaker |
@@ -62,7 +62,8 @@ usage: __main__.py [-h] [--name NAME] [--audio-input-device AUDIO_INPUT_DEVICE]
| `--mute-sound` | Sound played when muting the assistant | `sounds/mute_switch_on.flac` |
| `--unmute-sound` | Sound played when unmuting the assistant | `sounds/mute_switch_off.flac` |
| `--preferences-file` | Path to preferences JSON file | `preferences.json` |
| `--host` | Address for ESPHome server | `0.0.0.0` |
| `--host` | IP-Address for ESPHome server, use 0.0.0.0 for all | Autodetected |
| `--network-interface` | Network interface for ESPHome server | Autodetected |
| `--port` | Port for ESPHome server | 6053 |
| `--enable-thinking-sound` | Enable thinking sound on startup | False |
| `--debug` | Print DEBUG messages to console | False |
+62
View File
@@ -0,0 +1,62 @@
---
services:
# Fix permissions for wakeword_data and configuration volume
fix-permissions:
image: "ghcr.io/ohf-voice/linux-voice-assistant:latest"
entrypoint: []
command: "chown -R ${LVA_USER_ID}:${LVA_USER_GROUP} /app/local /app/configuration"
env_file: .env
group_add:
- audio
volumes:
# Wakeword data and configuration
- wakeword_data:/app/local
- configuration:/app/configuration
restart: "no"
# Linux Voice Assistant
linux-voice-assistant:
container_name: linux-voice-assistant
image: "ghcr.io/ohf-voice/linux-voice-assistant:latest"
restart: unless-stopped
network_mode: "host"
user: "${LVA_USER_ID}:${LVA_USER_GROUP}"
environment:
# Pulseaudio
- XDG_RUNTIME_DIR=${LVA_XDG_RUNTIME_DIR}
- PULSE_SERVER=${LVA_PULSE_SERVER}
env_file: .env
group_add:
- audio
# Priority for audio process
cap_add:
- SYS_NICE
volumes:
# Wakeword data and configuration
- wakeword_data:/app/local
- configuration:/app/configuration
# Time and zone
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
# Pulseaudio
# /run/user/1000:/run/user/1000
- ${LVA_XDG_RUNTIME_DIR}:${LVA_XDG_RUNTIME_DIR}
# Mount source code for development
- ./linux_voice_assistant:/app/linux_voice_assistant
depends_on:
- fix-permissions
# Health check for process
healthcheck:
test: ["CMD", "pgrep", "-f", "linux_voice_assistant"]
interval: 30s
timeout: 5s
retries: 3
start_period: 90s
#command:
# - --additional-parameters-here
# - --but-use-env-if-possible
volumes:
wakeword_data:
configuration:
+46 -29
View File
@@ -1,31 +1,6 @@
#!/bin/bash
set -e
### Generate name for this client
# Get active interface
IFACE=$(ip route | grep default | awk '{print $5}' | head -n 1)
# Check if IFACE is empty
if [ -z "$IFACE" ]; then
echo "No active network interface found."
fi
# Check if interface directory exists
if [ ! -e "/sys/class/net/$IFACE/address" ]; then
echo "Interface $IFACE does not exist or has no MAC address."
fi
# Read MAC address and remove ':'
MAC=$(cat /sys/class/net/$IFACE/address | tr -d ':')
if [ -z "$MAC" ]; then
echo "Could not read MAC address."
fi
# Generate CLIENT_NAME from it
TEMP_CLIENT_NAME="lva-${MAC}"
### Handlers
# Handle parameters
EXTRA_ARGS=()
@@ -34,7 +9,6 @@ if [ "$ENABLE_DEBUG" = "1" ]; then
EXTRA_ARGS+=( "--debug" )
fi
CLIENT_NAME=${CLIENT_NAME:-$TEMP_CLIENT_NAME}
if [ -n "${CLIENT_NAME}" ]; then
EXTRA_ARGS+=( "--name" "$CLIENT_NAME" )
fi
@@ -44,6 +18,15 @@ if [ -n "${PREFERENCES_FILE}" ]; then
EXTRA_ARGS+=( "--preferences-file" "$PREFERENCES_FILE" )
fi
if [ -n "${NETWORK_INTERFACE}" ]; then
EXTRA_ARGS+=( "--network-interface" "$NETWORK_INTERFACE" )
fi
# IP-ADDRESS
if [ -n "${HOST}" ]; then
EXTRA_ARGS+=( "--host" "$HOST" )
fi
PORT=${PORT:-6053}
if [ -n "${PORT}" ]; then
EXTRA_ARGS+=( "--port" "$PORT" )
@@ -61,6 +44,38 @@ if [ "$ENABLE_THINKING_SOUND" = "1" ]; then
EXTRA_ARGS+=( "--enable-thinking-sound" )
fi
if [ -n "${WAKE_MODEL}" ]; then
EXTRA_ARGS+=( "--wake-model" "$WAKE_MODEL" )
fi
if [ -n "${STOP_MODEL}" ]; then
EXTRA_ARGS+=( "--stop-model" "$STOP_MODEL" )
fi
if [ -n "${REFACTORY_SECONDS}" ]; then
EXTRA_ARGS+=( "--refractory-seconds" "$REFACTORY_SECONDS" )
fi
if [ -n "${WAKEUP_SOUND}" ]; then
EXTRA_ARGS+=( "--wakeup-sound" "$WAKEUP_SOUND" )
fi
if [ -n "${TIMER_FINISHED_SOUND}" ]; then
EXTRA_ARGS+=( "--timer-finished-sound" "$TIMER_FINISHED_SOUND" )
fi
if [ -n "${PROCESSING_SOUND}" ]; then
EXTRA_ARGS+=( "--processing-sound" "$PROCESSING_SOUND" )
fi
if [ -n "${MUTE_SOUND}" ]; then
EXTRA_ARGS+=( "--mute-sound" "$MUTE_SOUND" )
fi
if [ -n "${UNMUTE_SOUND}" ]; then
EXTRA_ARGS+=( "--unmute-sound" "$UNMUTE_SOUND" )
fi
### Wait for PulseAudio
# Wait for PulseAudio to be available before starting the application
@@ -113,7 +128,9 @@ if [ "$LIST_DEVICES" = "1" ]; then
./script/run "$@" "${EXTRA_ARGS[@]}" --list-input-devices
echo "list output devices"
./script/run "$@" "${EXTRA_ARGS[@]}" --list-output-devices
else
echo "starting application"
exec ./script/run "$@" "${EXTRA_ARGS[@]}"
echo "wait 20s and then starting the application"
sleep 20
fi
echo "starting application"
exec ./script/run "$@" "${EXTRA_ARGS[@]}"
+16 -30
View File
@@ -88,29 +88,11 @@ LVA_XDG_RUNTIME_DIR="/run/user/${USERLVA_USER_ID
# PORT="6053"
### Audio input device (optional):
# AUDIO_INPUT_DEVICE="default"
### Audio output device (optional):
# AUDIO_OUTPUT_DEVICE="default"
# Enable thinking sound (optional):
# ENABLE_THINKING_SOUND="1"
# Wake model (optional):
# WAKE-MODEL="okay_nabu"
# Refactory seconds (optional):
# REFACTORY_SECONDS="2"
# Sound files (optional):
# WAKEUP_SOUND="sounds/wake_word_triggered.flac"
# TIMER_FINISHED_SOUND="sounds/timer_finished.flac"
# PROCESSING_SOUND="sounds/processing.wav"
# MUTE_SOUND="sounds/mute_switch_on.flac"
# UNMUTE_SOUND="sounds/mute_switch_off.flac"
[...]
```
You can change various settings here, for example the audio sounds which are played when the wake word is detected or when the timer is finished.
Please use the original file from the repository. You can change various settings, for example the audio sounds which are played when the wake word is detected or when the timer is finished.
💡 **Note:** You can exit vim with `:wq` or `:q!` if you dont want to save the changes.
@@ -206,13 +188,15 @@ WorkingDirectory=/home/pi/linux-voice-assistant
Environment=PATH=/home/pi/linux-voice-assistant/.venv/bin:/usr/bin:/bin
# Environment=ENABLE_DEBUG="1"
# Environment=LIST_DEVICES="1"
Environment=USERLVA_USER_ID00"
Environment=LVA_USER_ID="1000"
Environment=LVA_USER_GROUP="1000"
Environment=CLIENT_NAME="My Voice Assistant Speaker"
Environment=LVA_PULSE_SERVER="unix:/run/user/${USERLVA_USER_IDlse/native"
Environment=LVA_XDG_RUNTIME_DIR="/run/user/${USERLVA_USER_ID
# Environment=CLIENT_NAME="My Voice Assistant Speaker"
Environment=LVA_PULSE_SERVER="unix:/run/user/${LVA_USER_ID}/pulse/native"
Environment=LVA_XDG_RUNTIME_DIR="/run/user/${LVA_USER_ID}"
Environment=PREFERENCES_FILE="/home/pi/linux-voice-assistant/preferences.json"
Environment=PORT="6053"
# Environment=NETWORK_INTERFACE="eth0"
# Environment=HOST="0.0.0.0"
# Environment=PORT="6053"
# Environment=AUDIO_INPUT_DEVICE="default"
# Environment=AUDIO_OUTPUT_DEVICE="default"
# Environment=ENABLE_THINKING_SOUND="1"
@@ -223,8 +207,8 @@ Environment=PORT="6053"
# Environment=PROCESSING_SOUND="sounds/processing.wav"
# Environment=MUTE_SOUND="sounds/mute_switch_on.flac"
# Environment=UNMUTE_SOUND="sounds/mute_switch_off.flac"
# ExecStart=/home/pi/linux-voice-assistant/docker-entrypoint.sh --additional-parameter-if-you-want
ExecStart=/home/pi/linux-voice-assistant/docker-entrypoint.sh
# ExecStart=/home/pi/linux-voice-assistant/docker-entrypoint.sh --additional-parameter-if-you-want
Restart=always
RestartSec=5
@@ -276,18 +260,20 @@ The following variables can be configured in the `.env` or in the service file:
| Variable | Default | Description |
|----------|---------|-------------|
| `USERLVA_USER_ID`1000` | User ID for the container (usually 1000 for the first user) |
| `USERLVA_USER_ID` | `1000` | User ID for the container (usually 1000 for the first user) |
| `GROUP_ID` | `1000` | GROUP ID for the container (usually 1000 for the first users group) |
| `CLIENT_NAME` | (optional) | Custom name for this voice assistant instance |
| `LVA_PULSE_SERVER` | `unix:/run/user/${USERLVA_USER_IDlse/native` | Path to the PulseAudio/PipeWire socket |
| `LVA_XDG_RUNTIME_DIR` | `/run/user/${USERLVA_USER_ID XDG runtime directory |
| `LVA_PULSE_SERVER` | `unix:/run/user/${LVA_USER_ID}/pulse/native` | Path to the PulseAudio/PipeWire socket |
| `LVA_XDG_RUNTIME_DIR` | `/run/user/${LVA_USER_ID}` | XDG runtime directory |
| `ENABLE_DEBUG` | (optional) | Set to "1" to enable debug mode |
| `LIST_DEVICES` | (optional) | Set to "1" to list audio devices instead of starting |
| `PREFERENCES_FILE` | (optional) | Path to a custom preferences JSON file |
| `NETWORK_INTERFACE` | Autodetected | network card for server |
| `HOST` | Autodetected | API server IP-Address, can be 0.0.0.0 for all interfaces, but only one network card works for MAC-ADDRESS and ESP protocol |
| `PORT` | `6053` | API server port |
| `AUDIO_INPUT_DEVICE` | `default` | Audio input device name |
| `AUDIO_OUTPUT_DEVICE` | `default` | Audio output device name |
| `ENABLE_THINKING_SOUND` | (optional) | Set to "1" to enable thinking sound |
| `ENABLE_THINKING_SOUND` | false | Set to "1" to enable thinking sound |
| `WAKE_MODEL` | `okay_nabu` | Wake word model to use |
| `REFACTORY_SECONDS` | `2` | Refractory period in seconds after wake word |
| `WAKEUP_SOUND` | `sounds/wake_word_triggered.flac` | Sound file for wake word triggered |
+72 -20
View File
@@ -18,8 +18,10 @@ from pyopen_wakeword import OpenWakeWord, OpenWakeWordFeatures
from .models import AvailableWakeWord, Preferences, ServerState, WakeWordType
from .mpv_player import MpvMediaPlayer
from .satellite import VoiceSatelliteProtocol
from .util import get_mac
from getmac import get_mac_address
from .zeroconf import HomeAssistantZeroconf
from .util import get_default_interface
from .util import get_default_ipv4
_LOGGER = logging.getLogger(__name__)
_MODULE_DIR = Path(__file__).parent
@@ -33,7 +35,9 @@ _SOUNDS_DIR = _REPO_DIR / "sounds"
async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--name")
parser.add_argument(
"--name"
)
parser.add_argument(
"--audio-input-device",
help="soundcard name for input device (see --list-input-devices)",
@@ -43,7 +47,11 @@ async def main() -> None:
action="store_true",
help="List audio input devices and exit",
)
parser.add_argument("--audio-input-block-size", type=int, default=1024)
parser.add_argument(
"--audio-input-block-size",
type=int,
default=1024
)
parser.add_argument(
"--audio-output-device",
help="mpv name for output device (see --list-output-devices)",
@@ -60,9 +68,15 @@ async def main() -> None:
help="Directory with wake word models (.tflite) and configs (.json)",
)
parser.add_argument(
"--wake-model", default="okay_nabu", help="Id of active wake model"
"--wake-model",
default="okay_nabu",
help="Id of active wake model"
)
parser.add_argument(
"--stop-model",
default="stop",
help="Id of stop model"
)
parser.add_argument("--stop-model", default="stop", help="Id of stop model")
parser.add_argument(
"--download-dir",
default=_REPO_DIR / "local",
@@ -74,12 +88,13 @@ async def main() -> None:
type=float,
help="Seconds before wake word can be activated again",
)
#
parser.add_argument(
"--wakeup-sound", default=str(_SOUNDS_DIR / "wake_word_triggered.flac")
"--wakeup-sound",
default=str(_SOUNDS_DIR / "wake_word_triggered.flac")
)
parser.add_argument(
"--timer-finished-sound", default=str(_SOUNDS_DIR / "timer_finished.flac")
"--timer-finished-sound",
default=str(_SOUNDS_DIR / "timer_finished.flac")
)
parser.add_argument(
"--processing-sound",
@@ -96,17 +111,20 @@ async def main() -> None:
default=str(_SOUNDS_DIR / "mute_switch_off.flac"),
help="Sound to play when unmuting the assistant",
)
#
parser.add_argument("--preferences-file", default=_REPO_DIR / "preferences.json")
#
parser.add_argument(
"--host",
default="0.0.0.0",
help="Address for ESPHome server (default: 0.0.0.0)",
help="IP-Address for ESPHome server", # 0.0.0.0 is IPv4, None is all interfaces
)
parser.add_argument(
"--network-interface",
help="Network interface to use for ESPHome server (default: will be automatically detected)",
)
# Note that default port is also set in docker-entrypoint.sh
parser.add_argument(
"--port", type=int, default=6053, help="Port for ESPHome server (default: 6053)"
"--port", type=int,
default=6053,
help="Port for ESPHome server (default: 6053)"
)
parser.add_argument(
"--enable-thinking-sound",
@@ -139,9 +157,41 @@ async def main() -> None:
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
_LOGGER.debug(args)
if not args.name:
parser.error("the following arguments are required: --name")
# Resolve network interface for mac-adress detection
if not args.network_interface:
print("No network interface specified, try to detect default interface")
network_interface = get_default_interface()
print(f"Default interface detected:", network_interface)
else:
print("Network interface specified")
print(f"Using network interface: ", network_interface)
network_interface = args.network_interface
# Resolve ip_address where the application will be listening
if not args.host:
print("No host (ip-address) specified, try to detect IP-Address")
host_ip_address = get_default_ipv4(network_interface)
print(f"IP-Address detected: ", host_ip_address)
else:
print("Host specified")
print(f"Using host: ", args.host)
host_ip_address = args.host
# Resolve mac
mac_address = get_mac_address(interface=network_interface)
mac_address_clean = mac_address.replace(":", "")
# Resolve name
if not args.name:
print("No name specified, try to autogenerate name")
device_name = f"lva-{mac_address_clean}"
print(f"Name autogenerated: {device_name}")
else:
print("Name specified")
print(f"Using name: {args.name}")
device_name = args.name
# Resolve download dir
args.download_dir = Path(args.download_dir)
args.download_dir.mkdir(parents=True, exist_ok=True)
@@ -237,8 +287,10 @@ async def main() -> None:
assert stop_model is not None
state = ServerState(
name=args.name,
mac_address=get_mac(),
name=device_name,
network_interface=network_interface,
mac_address=get_mac_address(interface=network_interface),
ip_address=host_ip_address,
audio_queue=Queue(),
entities=[],
available_wake_words=available_wake_words,
@@ -270,16 +322,16 @@ async def main() -> None:
loop = asyncio.get_running_loop()
server = await loop.create_server(
lambda: VoiceSatelliteProtocol(state), host=args.host, port=args.port
lambda: VoiceSatelliteProtocol(state), host=host_ip_address, port=args.port
)
# Auto discovery (zeroconf, mDNS)
discovery = HomeAssistantZeroconf(port=args.port, name=args.name)
discovery = HomeAssistantZeroconf(port=args.port, name=state.name, mac_address=state.mac_address, host_ip_address=host_ip_address)
await discovery.register_server()
try:
async with server:
_LOGGER.info("Server started (host=%s, port=%s)", args.host, args.port)
_LOGGER.info("Server started (host=%s, port=%s)", host_ip_address, args.port)
await server.serve_forever()
except KeyboardInterrupt:
pass
+2
View File
@@ -64,6 +64,8 @@ class Preferences:
class ServerState:
name: str
mac_address: str
ip_address: str
network_interface: str
audio_queue: "Queue[Optional[bytes]]"
entities: "List[ESPHomeEntity]"
available_wake_words: "Dict[str, AvailableWakeWord]"
+35 -7
View File
@@ -1,16 +1,44 @@
"""Utility methods."""
import uuid
# netifaces lib is from netifaces2
import netifaces
from collections.abc import Callable
from typing import Optional
def get_mac() -> str:
mac = uuid.getnode()
mac_str = ":".join(f"{(mac >> i) & 0xff:02x}" for i in range(40, -1, -8))
return mac_str
def call_all(*callables: Optional[Callable[[], None]]) -> None:
for item in filter(None, callables):
item()
def get_default_interface():
"""Return the default network interface name, or None if not found."""
default_gateway = netifaces.default_gateway()
if not default_gateway:
print("No default gateway found")
return None
# default_gateway is e.g. {InterfaceType.AF_INET: ('192.168.33.1', 'wlp0s20f3')}
gateway_info = default_gateway.get(netifaces.AF_INET)
if not gateway_info:
print("No default IPv4 gateway found")
return None
# gateway_info is a tuple: (gateway_ip, interface_name)
interface_name = gateway_info[1]
#print(f"Default interface: {interface_name}")
return interface_name
def get_default_ipv4(interface: str):
if not interface:
return None
addresses = netifaces.ifaddresses(interface)
ipv4_info = addresses.get(netifaces.AF_INET)
if not ipv4_info:
return None
return ipv4_info[0]["addr"]
+6 -22
View File
@@ -18,20 +18,13 @@ MDNS_TARGET_IP = "224.0.0.251"
class HomeAssistantZeroconf:
def __init__(
self, port: int, name: Optional[str] = None, host: Optional[str] = None
self, port: int, mac_address: str, host_ip_address: str, name: Optional[str] = None, host: Optional[str] = None
) -> None:
self.port = port
self.name = name or _get_mac_address()
self.mac_address = mac_address
self.name = name or self.mac_address
self.host_ip_address = host_ip_address
if not host:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
test_sock.setblocking(False)
test_sock.connect((MDNS_TARGET_IP, 1))
host = test_sock.getsockname()[0]
_LOGGER.debug("Detected IP: %s", host)
assert host
self.host = host
self._aiozc = AsyncZeroconf()
async def register_server(self) -> None:
@@ -39,11 +32,11 @@ class HomeAssistantZeroconf:
service_info = AsyncServiceInfo(
"_esphomelib._tcp.local.",
f"{self.name}._esphomelib._tcp.local.",
addresses=[socket.inet_aton(self.host)],
addresses=[socket.inet_aton(self.host_ip_address)],
port=self.port,
properties={
"version": "2025.9.0",
"mac": _get_mac_address(),
"mac": self.mac_address,
"board": "host",
"platform": "HOST",
"network": "ethernet", # or "wifi"
@@ -53,12 +46,3 @@ class HomeAssistantZeroconf:
await self._aiozc.async_register_service(service_info)
_LOGGER.debug("Zeroconf discovery enabled: %s", service_info)
def _get_mac_address() -> str:
"""Return MAC address formatted as hex with no colons."""
return "".join(
# pylint: disable=consider-using-f-string
["{:02x}".format((uuid.getnode() >> ele) & 0xFF) for ele in range(0, 8 * 6, 8)][
::-1
]
)
+2
View File
@@ -26,12 +26,14 @@ classifiers = [
requires-python = ">=3.9.0"
dependencies = [
"aioesphomeapi==42.7.0",
"netifaces2==0.0.22",
"soundcard<1",
"numpy>=2,<3",
"pymicro-wakeword>=2,<3",
"pyopen-wakeword>=1,<2",
"python-mpv>=1,<2",
"zeroconf<1",
"getmac<1",
]
[project.optional-dependencies]