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

+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
]
)