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:
1 parent
f72e5c8d30
commit
4052daae01
11 files changed
+256
-113
No files matched your search
@@ -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
|
||||
|
||||
@@ -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]"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
Reference in new issue
Block a user