diff --git a/.env.example b/.env.example index 31621b3..0a7f2de 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/Dockerfile b/Dockerfile index 110688e..082deb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,7 @@ apt-get install --yes --no-install-recommends \ libasound2-plugins \ ca-certificates \ iproute2 \ + vim \ procps && \ apt-get clean diff --git a/README.md b/README.md index f0677aa..30d144c 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..9d92e8d --- /dev/null +++ b/docker-compose.dev.yml @@ -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: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2176081..2ccd30f 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -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[@]}" diff --git a/docs/install_application.md b/docs/install_application.md index 54ea13b..74416a8 100644 --- a/docs/install_application.md +++ b/docs/install_application.md @@ -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 | diff --git a/linux_voice_assistant/__main__.py b/linux_voice_assistant/__main__.py index e7d5187..97eb516 100644 --- a/linux_voice_assistant/__main__.py +++ b/linux_voice_assistant/__main__.py @@ -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 diff --git a/linux_voice_assistant/models.py b/linux_voice_assistant/models.py index 805a2aa..dc433a6 100644 --- a/linux_voice_assistant/models.py +++ b/linux_voice_assistant/models.py @@ -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]" diff --git a/linux_voice_assistant/util.py b/linux_voice_assistant/util.py index a277388..77131cf 100644 --- a/linux_voice_assistant/util.py +++ b/linux_voice_assistant/util.py @@ -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"] diff --git a/linux_voice_assistant/zeroconf.py b/linux_voice_assistant/zeroconf.py index 6baeeb4..cff484a 100644 --- a/linux_voice_assistant/zeroconf.py +++ b/linux_voice_assistant/zeroconf.py @@ -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 - ] - ) diff --git a/pyproject.toml b/pyproject.toml index 03e57f6..5896f03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]