././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1731838012.4680002 usb_monitor-1.23/0000755000076500000240000000000014716340074012667 5ustar00harustaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717225473.0 usb_monitor-1.23/LICENSE0000644000076500000240000000205414626544001013671 0ustar00harustaffMIT License Copyright (c) 2023 Eric CaƱas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1731838012.4676502 usb_monitor-1.23/PKG-INFO0000644000076500000240000002763114716340074013775 0ustar00harustaffMetadata-Version: 2.1 Name: usb-monitor Version: 1.23 Summary: USBMonitor is an easy-to-use cross-platform library for USB device monitoring that simplifies tracking of connections, disconnections, and examination of connected device attributes on Windows, Linux and MacOs, freeing the user from platform-specific details or incompatibilities. Home-page: https://github.com/Eric-Canas/USBMonitor Author: Eric-Canas Author-email: eric@ericcanas.com License: MIT Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Operating System :: OS Independent Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Intended Audience :: Developers Classifier: Topic :: System :: Hardware Classifier: Topic :: System :: Hardware :: Hardware Drivers Classifier: Topic :: System :: Systems Administration Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Operating System Classifier: Topic :: Software Development :: Embedded Systems Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: pyudev; platform_system == "Linux" Requires-Dist: pywin32; platform_system == "Windows" Requires-Dist: wmi; platform_system == "Windows" # USBMonitor USBMonitor **USBMonitor** is a versatile **cross-platform** library that simplifies **USB device monitoring** for _Windows_, _Linux_ and _MacOS_ systems. It enables developers to effortlessly track device **connections**, **disconnections**, and access to all connected device **attributes**. With **USBMonitor**, developers can stay up-to-date with any changes in the connected USB devices, allowing them to **trigger specific actions** whenever a USB device is connected or disconnected. By ensuring **consistent functionality across various operating systems**, **USBMonitor** removes the need to address platform-specific quirks, inconsistencies, or incompatibilities, resulting in a smooth and efficient USB device management experience. The uniformity in functionality significantly enhances **code compatibility**, minimizing the risk of **code issues** or **unexpected breaks** when moving between platforms. At its core, **USBMonitor** utilizes pyudev (for Linux environments), WMI (for Windows environments), and the I/O Registry (for MacOs environments). Handling all the low-level intricacies and translating OS-specific information to ensure consistency across all systems. ## Installation To install **USBMonitor**, simply run: ```bash pip install usb-monitor ``` ## Usage Using **USBMonitor** is both simple and straight-forward. In most cases, you'll just want to start the [monitoring _Daemon_](#usbmonitorstart_monitoringon_connect--none-on_disconnect--none-check_every_seconds--05), defining the `on_connect` and `on_disconnect` callback functions to manage events when a USB device connects or disconnects. Here's a basic example: ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID device_info_str = lambda device_info: f"{device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})" # Define the `on_connect` and `on_disconnect` callbacks on_connect = lambda device_id, device_info: print(f"Connected: {device_info_str(device_info=device_info)}") on_disconnect = lambda device_id, device_info: print(f"Disconnected: {device_info_str(device_info=device_info)}") # Create the USBMonitor instance monitor = USBMonitor() # Start the daemon monitor.start_monitoring(on_connect=on_connect, on_disconnect=on_disconnect) # ... Rest of your code ... # If you don't need it anymore stop the daemon monitor.stop_monitoring() ``` Output Linux | Windows :---: | :---: ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/linux_monitor.gif) | ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/windows_monitor.gif) Sometimes, when initializing your software, you may seek to confirm which USB devices are indeed connected. ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID # Create the USBMonitor instance monitor = USBMonitor() # Get the current devices devices_dict = monitor.get_available_devices() # Print them for device_id, device_info in devices_dict.items(): print(f"{device_id} -- {device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})") ``` Output ```bash /dev/bus/usb/001/001 -- xHCI_Host_Controller (0002 - 1d6b) /dev/bus/usb/001/002 -- USB2.0_Hub (3431 - 2109) /dev/bus/usb/001/003 -- USB_Optical_Mouse (c077 - 046d) /dev/bus/usb/001/004 -- USB_Compliant_Keypad (9881 - 05a4) /dev/bus/usb/002/001 -- xHCI_Host_Controller (0003 - 1d6b) ``` ## API Reference ### USBMonitor(filter_devices = None) Initialize the USBMonitor instance. It will allow to inspect and monitor connected devices - `filter_devices`: **tuple[dict[str, str]] | None**. A tuple of dictionaries containing the device attributes to filter. If passed, it will only return and monitor devices that match any of the specified filters. For example, if you want to only retrieve and track devices with 'ID_VENDOR_FROM_DATABASE' = 'Realtek' or the device with 'ID_VENDOR_ID' = '1234' and 'ID_MODEL_ID' = '1A2B' you should instantiate with: `USBMonitor(filter_devices=({'ID_VENDOR_FROM_DATABASE': 'Realtek'}, {'ID_VENDOR_ID': '1234', 'ID_MODEL_ID': '1A2B'}))`. Default value is None. ### USBMonitor.start_monitoring(on_connect = None, on_disconnect = None, check_every_seconds = 0.5) Starts a daemon that continuously monitors the connected USB devices in order to detect new connections or disconnections. When a device is disconnected, the `on_disconnect` callback function is invoked with the Device ID as the first argument and the [dictionary of device information](#device-properties) as the second argument. Similarly, when a new device is connected, the `on_connect` callback function is called with the same arguments. This allows developers to promptly respond to any changes in the connected USB devices and perform necessary actions. - `on_connect`: **callable | None**. The function to call every time a device is **added**. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call every time a device is **removed**. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `check_every_seconds`: **int | float**. Seconds to wait between each check for changes in the USB devices. Default value is 0.5 seconds. ### USBMonitor.stop_monitoring(warn_if_was_stopped=True) Stops the monitoring of USB devices. This function will **stop** the daemon launched by `USBMonitor.start_monitoring` - `warn_if_was_stopped`: **bool**. If set to `True`, this function will issue a warning if the monitoring of USB devices was already stopped (the daemon was not running). ### USBMonitor.get_available_devices() Returns a dictionary of the currently available devices, where the key is the `Device ID` and the value is a [dictionary containing the device's information](#device-properties). All the keys of this dictionary can be found at `attributes.DEVICE_ATTRIBUTES`. They always correspond with the default Linux device properties (independently of the OS where the library is running). - Returns: **dict[str, dict[str, str|tuple[str, ...]]]**: A dictionary containing the currently available devices. All values are strings except for `ID_USB_INTERFACES`, which is a `tuple` of `string` ### USBMonitor.changes_from_last_check(update_last_check_devices = True) Returns a tuple of two dictionaries, one containing the devices that have been *removed* since the last check, and another one containing the devices that have been *added*. Both dictionaries will have the `Device ID` as key and all the device information as value. Remember that all the [keys of this dictionary](#device-properties) can be found at can be found at `attributes.DEVICE_ATTRIBUTES`. - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. - Returns: **tuple[dict[str, dict[str, str|tuple[str, ...]]], dict[str, dict[str, str|tuple[str, ...]]]]**: A `tuple` containing two `dictionaries`. The first `dictionary` contains the information of the devices that were **removed** since the last check and the second dictionary contains the information of the new **added** devices. All values are `strings` except for `ID_USB_INTERFACES`, which is a `tuple` of `string`. ### USBMonitor.check_changes(on_connect = None, on_disconnect = None, update_last_check_devices = True) Checks for any new connections or disconnections of USB devices since the last check. If a device has been removed, the `on_disconnect` function will be called with the `Device ID` as the first argument and the [dictionary with the device's information](#device-properties) as the second argument. The same will occur with the `on_connect` function if any new device have been added. Internally this function will just run `USBMonitor.changes_from_last_check` and will execute the callbacks for each returned device - `on_connect`: **callable | None**. The function to call when a device is added. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call when a device is removed. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. ### Device Properties The `device_info` returned by most functions will contain the following information: Key | Value Description | Example :-- | :-- | :-- `'ID_MODEL_ID'` | The product ID of the USB device. | `'0892'` `'ID_MODEL'` | The name of the USB device model. | `'HD_Pro_Webcam_C920'` `'ID_MODEL_FROM_DATABASE'` | Device model name, retrieved from the device database.| `'OrbiCam'` `'ID_VENDOR'` | The name of the USB device vendor | `'046d'` `'ID_VENDOR_ID'` | The vendor ID of the USB device. | `'046d'` `'ID_VENDOR_FROM_DATABASE'` | USB device vendor's name, from the device database. | `'Logitech, Inc.'` `'ID_USB_INTERFACES'` | A `tuple` representing the USB device's interfaces. | `('0e0100', '0e0200', '010100', '010200')` `'DEVNAME'` | The device name or path | `'/dev/bus/usb/001/003'` `'DEVTYPE'` | Should always be `'usb_device'`. | `'usb_device'` `'ID_SERIAL'` | The serial number of the USB device. | `'92C5B92F'` Note that, depending on the device and the OS, some of this information may be incomplete or certain attributes may overlap with others. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731836539.0 usb_monitor-1.23/README.md0000644000076500000240000002447214716335173014163 0ustar00harustaff# USBMonitor USBMonitor **USBMonitor** is a versatile **cross-platform** library that simplifies **USB device monitoring** for _Windows_, _Linux_ and _MacOS_ systems. It enables developers to effortlessly track device **connections**, **disconnections**, and access to all connected device **attributes**. With **USBMonitor**, developers can stay up-to-date with any changes in the connected USB devices, allowing them to **trigger specific actions** whenever a USB device is connected or disconnected. By ensuring **consistent functionality across various operating systems**, **USBMonitor** removes the need to address platform-specific quirks, inconsistencies, or incompatibilities, resulting in a smooth and efficient USB device management experience. The uniformity in functionality significantly enhances **code compatibility**, minimizing the risk of **code issues** or **unexpected breaks** when moving between platforms. At its core, **USBMonitor** utilizes pyudev (for Linux environments), WMI (for Windows environments), and the I/O Registry (for MacOs environments). Handling all the low-level intricacies and translating OS-specific information to ensure consistency across all systems. ## Installation To install **USBMonitor**, simply run: ```bash pip install usb-monitor ``` ## Usage Using **USBMonitor** is both simple and straight-forward. In most cases, you'll just want to start the [monitoring _Daemon_](#usbmonitorstart_monitoringon_connect--none-on_disconnect--none-check_every_seconds--05), defining the `on_connect` and `on_disconnect` callback functions to manage events when a USB device connects or disconnects. Here's a basic example: ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID device_info_str = lambda device_info: f"{device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})" # Define the `on_connect` and `on_disconnect` callbacks on_connect = lambda device_id, device_info: print(f"Connected: {device_info_str(device_info=device_info)}") on_disconnect = lambda device_id, device_info: print(f"Disconnected: {device_info_str(device_info=device_info)}") # Create the USBMonitor instance monitor = USBMonitor() # Start the daemon monitor.start_monitoring(on_connect=on_connect, on_disconnect=on_disconnect) # ... Rest of your code ... # If you don't need it anymore stop the daemon monitor.stop_monitoring() ``` Output Linux | Windows :---: | :---: ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/linux_monitor.gif) | ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/windows_monitor.gif) Sometimes, when initializing your software, you may seek to confirm which USB devices are indeed connected. ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID # Create the USBMonitor instance monitor = USBMonitor() # Get the current devices devices_dict = monitor.get_available_devices() # Print them for device_id, device_info in devices_dict.items(): print(f"{device_id} -- {device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})") ``` Output ```bash /dev/bus/usb/001/001 -- xHCI_Host_Controller (0002 - 1d6b) /dev/bus/usb/001/002 -- USB2.0_Hub (3431 - 2109) /dev/bus/usb/001/003 -- USB_Optical_Mouse (c077 - 046d) /dev/bus/usb/001/004 -- USB_Compliant_Keypad (9881 - 05a4) /dev/bus/usb/002/001 -- xHCI_Host_Controller (0003 - 1d6b) ``` ## API Reference ### USBMonitor(filter_devices = None) Initialize the USBMonitor instance. It will allow to inspect and monitor connected devices - `filter_devices`: **tuple[dict[str, str]] | None**. A tuple of dictionaries containing the device attributes to filter. If passed, it will only return and monitor devices that match any of the specified filters. For example, if you want to only retrieve and track devices with 'ID_VENDOR_FROM_DATABASE' = 'Realtek' or the device with 'ID_VENDOR_ID' = '1234' and 'ID_MODEL_ID' = '1A2B' you should instantiate with: `USBMonitor(filter_devices=({'ID_VENDOR_FROM_DATABASE': 'Realtek'}, {'ID_VENDOR_ID': '1234', 'ID_MODEL_ID': '1A2B'}))`. Default value is None. ### USBMonitor.start_monitoring(on_connect = None, on_disconnect = None, check_every_seconds = 0.5) Starts a daemon that continuously monitors the connected USB devices in order to detect new connections or disconnections. When a device is disconnected, the `on_disconnect` callback function is invoked with the Device ID as the first argument and the [dictionary of device information](#device-properties) as the second argument. Similarly, when a new device is connected, the `on_connect` callback function is called with the same arguments. This allows developers to promptly respond to any changes in the connected USB devices and perform necessary actions. - `on_connect`: **callable | None**. The function to call every time a device is **added**. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call every time a device is **removed**. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `check_every_seconds`: **int | float**. Seconds to wait between each check for changes in the USB devices. Default value is 0.5 seconds. ### USBMonitor.stop_monitoring(warn_if_was_stopped=True) Stops the monitoring of USB devices. This function will **stop** the daemon launched by `USBMonitor.start_monitoring` - `warn_if_was_stopped`: **bool**. If set to `True`, this function will issue a warning if the monitoring of USB devices was already stopped (the daemon was not running). ### USBMonitor.get_available_devices() Returns a dictionary of the currently available devices, where the key is the `Device ID` and the value is a [dictionary containing the device's information](#device-properties). All the keys of this dictionary can be found at `attributes.DEVICE_ATTRIBUTES`. They always correspond with the default Linux device properties (independently of the OS where the library is running). - Returns: **dict[str, dict[str, str|tuple[str, ...]]]**: A dictionary containing the currently available devices. All values are strings except for `ID_USB_INTERFACES`, which is a `tuple` of `string` ### USBMonitor.changes_from_last_check(update_last_check_devices = True) Returns a tuple of two dictionaries, one containing the devices that have been *removed* since the last check, and another one containing the devices that have been *added*. Both dictionaries will have the `Device ID` as key and all the device information as value. Remember that all the [keys of this dictionary](#device-properties) can be found at can be found at `attributes.DEVICE_ATTRIBUTES`. - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. - Returns: **tuple[dict[str, dict[str, str|tuple[str, ...]]], dict[str, dict[str, str|tuple[str, ...]]]]**: A `tuple` containing two `dictionaries`. The first `dictionary` contains the information of the devices that were **removed** since the last check and the second dictionary contains the information of the new **added** devices. All values are `strings` except for `ID_USB_INTERFACES`, which is a `tuple` of `string`. ### USBMonitor.check_changes(on_connect = None, on_disconnect = None, update_last_check_devices = True) Checks for any new connections or disconnections of USB devices since the last check. If a device has been removed, the `on_disconnect` function will be called with the `Device ID` as the first argument and the [dictionary with the device's information](#device-properties) as the second argument. The same will occur with the `on_connect` function if any new device have been added. Internally this function will just run `USBMonitor.changes_from_last_check` and will execute the callbacks for each returned device - `on_connect`: **callable | None**. The function to call when a device is added. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call when a device is removed. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. ### Device Properties The `device_info` returned by most functions will contain the following information: Key | Value Description | Example :-- | :-- | :-- `'ID_MODEL_ID'` | The product ID of the USB device. | `'0892'` `'ID_MODEL'` | The name of the USB device model. | `'HD_Pro_Webcam_C920'` `'ID_MODEL_FROM_DATABASE'` | Device model name, retrieved from the device database.| `'OrbiCam'` `'ID_VENDOR'` | The name of the USB device vendor | `'046d'` `'ID_VENDOR_ID'` | The vendor ID of the USB device. | `'046d'` `'ID_VENDOR_FROM_DATABASE'` | USB device vendor's name, from the device database. | `'Logitech, Inc.'` `'ID_USB_INTERFACES'` | A `tuple` representing the USB device's interfaces. | `('0e0100', '0e0200', '010100', '010200')` `'DEVNAME'` | The device name or path | `'/dev/bus/usb/001/003'` `'DEVTYPE'` | Should always be `'usb_device'`. | `'usb_device'` `'ID_SERIAL'` | The serial number of the USB device. | `'92C5B92F'` Note that, depending on the device and the OS, some of this information may be incomplete or certain attributes may overlap with others. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1731838012.468067 usb_monitor-1.23/setup.cfg0000644000076500000240000000004614716340074014510 0ustar00harustaff[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731837943.0 usb_monitor-1.23/setup.py0000644000076500000240000000345414716337767014426 0ustar00harustafffrom setuptools import setup, find_packages setup( name='usb-monitor', version='1.23', author='Eric-Canas', author_email='eric@ericcanas.com', url='https://github.com/Eric-Canas/USBMonitor', description='USBMonitor is an easy-to-use cross-platform library for USB device monitoring that simplifies ' 'tracking of connections, disconnections, and examination of connected device attributes on ' 'Windows, Linux and MacOs, freeing the user from platform-specific details or incompatibilities.', long_description=open('README.md', 'r').read(), long_description_content_type='text/markdown', license='MIT', packages=find_packages(), python_requires='>=3.6', install_requires=[ 'pyudev; platform_system=="Linux"', 'pywin32; platform_system=="Windows"', 'wmi; platform_system=="Windows"', ], classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Operating System :: OS Independent', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS', 'Intended Audience :: Developers', 'Topic :: System :: Hardware', 'Topic :: System :: Hardware :: Hardware Drivers', 'Topic :: System :: Systems Administration', 'Topic :: System :: Monitoring', 'Topic :: System :: Operating System', 'Topic :: Software Development :: Embedded Systems', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Utilities', ], )././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1731838012.4672494 usb_monitor-1.23/usb_monitor.egg-info/0000755000076500000240000000000014716340074016721 5ustar00harustaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731838012.0 usb_monitor-1.23/usb_monitor.egg-info/PKG-INFO0000644000076500000240000002763114716340074020027 0ustar00harustaffMetadata-Version: 2.1 Name: usb-monitor Version: 1.23 Summary: USBMonitor is an easy-to-use cross-platform library for USB device monitoring that simplifies tracking of connections, disconnections, and examination of connected device attributes on Windows, Linux and MacOs, freeing the user from platform-specific details or incompatibilities. Home-page: https://github.com/Eric-Canas/USBMonitor Author: Eric-Canas Author-email: eric@ericcanas.com License: MIT Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Operating System :: OS Independent Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX :: Linux Classifier: Operating System :: MacOS Classifier: Intended Audience :: Developers Classifier: Topic :: System :: Hardware Classifier: Topic :: System :: Hardware :: Hardware Drivers Classifier: Topic :: System :: Systems Administration Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Operating System Classifier: Topic :: Software Development :: Embedded Systems Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.6 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: pyudev; platform_system == "Linux" Requires-Dist: pywin32; platform_system == "Windows" Requires-Dist: wmi; platform_system == "Windows" # USBMonitor USBMonitor **USBMonitor** is a versatile **cross-platform** library that simplifies **USB device monitoring** for _Windows_, _Linux_ and _MacOS_ systems. It enables developers to effortlessly track device **connections**, **disconnections**, and access to all connected device **attributes**. With **USBMonitor**, developers can stay up-to-date with any changes in the connected USB devices, allowing them to **trigger specific actions** whenever a USB device is connected or disconnected. By ensuring **consistent functionality across various operating systems**, **USBMonitor** removes the need to address platform-specific quirks, inconsistencies, or incompatibilities, resulting in a smooth and efficient USB device management experience. The uniformity in functionality significantly enhances **code compatibility**, minimizing the risk of **code issues** or **unexpected breaks** when moving between platforms. At its core, **USBMonitor** utilizes pyudev (for Linux environments), WMI (for Windows environments), and the I/O Registry (for MacOs environments). Handling all the low-level intricacies and translating OS-specific information to ensure consistency across all systems. ## Installation To install **USBMonitor**, simply run: ```bash pip install usb-monitor ``` ## Usage Using **USBMonitor** is both simple and straight-forward. In most cases, you'll just want to start the [monitoring _Daemon_](#usbmonitorstart_monitoringon_connect--none-on_disconnect--none-check_every_seconds--05), defining the `on_connect` and `on_disconnect` callback functions to manage events when a USB device connects or disconnects. Here's a basic example: ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID device_info_str = lambda device_info: f"{device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})" # Define the `on_connect` and `on_disconnect` callbacks on_connect = lambda device_id, device_info: print(f"Connected: {device_info_str(device_info=device_info)}") on_disconnect = lambda device_id, device_info: print(f"Disconnected: {device_info_str(device_info=device_info)}") # Create the USBMonitor instance monitor = USBMonitor() # Start the daemon monitor.start_monitoring(on_connect=on_connect, on_disconnect=on_disconnect) # ... Rest of your code ... # If you don't need it anymore stop the daemon monitor.stop_monitoring() ``` Output Linux | Windows :---: | :---: ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/linux_monitor.gif) | ![](https://raw.githubusercontent.com/Eric-Canas/USBMonitor/main/resources/windows_monitor.gif) Sometimes, when initializing your software, you may seek to confirm which USB devices are indeed connected. ```python from usbmonitor import USBMonitor from usbmonitor.attributes import ID_MODEL, ID_MODEL_ID, ID_VENDOR_ID # Create the USBMonitor instance monitor = USBMonitor() # Get the current devices devices_dict = monitor.get_available_devices() # Print them for device_id, device_info in devices_dict.items(): print(f"{device_id} -- {device_info[ID_MODEL]} ({device_info[ID_MODEL_ID]} - {device_info[ID_VENDOR_ID]})") ``` Output ```bash /dev/bus/usb/001/001 -- xHCI_Host_Controller (0002 - 1d6b) /dev/bus/usb/001/002 -- USB2.0_Hub (3431 - 2109) /dev/bus/usb/001/003 -- USB_Optical_Mouse (c077 - 046d) /dev/bus/usb/001/004 -- USB_Compliant_Keypad (9881 - 05a4) /dev/bus/usb/002/001 -- xHCI_Host_Controller (0003 - 1d6b) ``` ## API Reference ### USBMonitor(filter_devices = None) Initialize the USBMonitor instance. It will allow to inspect and monitor connected devices - `filter_devices`: **tuple[dict[str, str]] | None**. A tuple of dictionaries containing the device attributes to filter. If passed, it will only return and monitor devices that match any of the specified filters. For example, if you want to only retrieve and track devices with 'ID_VENDOR_FROM_DATABASE' = 'Realtek' or the device with 'ID_VENDOR_ID' = '1234' and 'ID_MODEL_ID' = '1A2B' you should instantiate with: `USBMonitor(filter_devices=({'ID_VENDOR_FROM_DATABASE': 'Realtek'}, {'ID_VENDOR_ID': '1234', 'ID_MODEL_ID': '1A2B'}))`. Default value is None. ### USBMonitor.start_monitoring(on_connect = None, on_disconnect = None, check_every_seconds = 0.5) Starts a daemon that continuously monitors the connected USB devices in order to detect new connections or disconnections. When a device is disconnected, the `on_disconnect` callback function is invoked with the Device ID as the first argument and the [dictionary of device information](#device-properties) as the second argument. Similarly, when a new device is connected, the `on_connect` callback function is called with the same arguments. This allows developers to promptly respond to any changes in the connected USB devices and perform necessary actions. - `on_connect`: **callable | None**. The function to call every time a device is **added**. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call every time a device is **removed**. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `check_every_seconds`: **int | float**. Seconds to wait between each check for changes in the USB devices. Default value is 0.5 seconds. ### USBMonitor.stop_monitoring(warn_if_was_stopped=True) Stops the monitoring of USB devices. This function will **stop** the daemon launched by `USBMonitor.start_monitoring` - `warn_if_was_stopped`: **bool**. If set to `True`, this function will issue a warning if the monitoring of USB devices was already stopped (the daemon was not running). ### USBMonitor.get_available_devices() Returns a dictionary of the currently available devices, where the key is the `Device ID` and the value is a [dictionary containing the device's information](#device-properties). All the keys of this dictionary can be found at `attributes.DEVICE_ATTRIBUTES`. They always correspond with the default Linux device properties (independently of the OS where the library is running). - Returns: **dict[str, dict[str, str|tuple[str, ...]]]**: A dictionary containing the currently available devices. All values are strings except for `ID_USB_INTERFACES`, which is a `tuple` of `string` ### USBMonitor.changes_from_last_check(update_last_check_devices = True) Returns a tuple of two dictionaries, one containing the devices that have been *removed* since the last check, and another one containing the devices that have been *added*. Both dictionaries will have the `Device ID` as key and all the device information as value. Remember that all the [keys of this dictionary](#device-properties) can be found at can be found at `attributes.DEVICE_ATTRIBUTES`. - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. - Returns: **tuple[dict[str, dict[str, str|tuple[str, ...]]], dict[str, dict[str, str|tuple[str, ...]]]]**: A `tuple` containing two `dictionaries`. The first `dictionary` contains the information of the devices that were **removed** since the last check and the second dictionary contains the information of the new **added** devices. All values are `strings` except for `ID_USB_INTERFACES`, which is a `tuple` of `string`. ### USBMonitor.check_changes(on_connect = None, on_disconnect = None, update_last_check_devices = True) Checks for any new connections or disconnections of USB devices since the last check. If a device has been removed, the `on_disconnect` function will be called with the `Device ID` as the first argument and the [dictionary with the device's information](#device-properties) as the second argument. The same will occur with the `on_connect` function if any new device have been added. Internally this function will just run `USBMonitor.changes_from_last_check` and will execute the callbacks for each returned device - `on_connect`: **callable | None**. The function to call when a device is added. It is expected to have the following format `on_connect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `on_disconnect`: **callable | None**. The function to call when a device is removed. It is expected to have the following format `on_disconnect(device_id: str, device_info: dict[str, dict[str, str|tuple[str, ...]]])` - `update_last_check_devices`: **bool**. If `True` it will update the internal `USBMonitor.last_check_devices` attribute. So the next time you'll call this method, it will check for differences against the devices found in that current call. If `False` it won't update the `USBMonitor.last_check_devices` attribute. ### Device Properties The `device_info` returned by most functions will contain the following information: Key | Value Description | Example :-- | :-- | :-- `'ID_MODEL_ID'` | The product ID of the USB device. | `'0892'` `'ID_MODEL'` | The name of the USB device model. | `'HD_Pro_Webcam_C920'` `'ID_MODEL_FROM_DATABASE'` | Device model name, retrieved from the device database.| `'OrbiCam'` `'ID_VENDOR'` | The name of the USB device vendor | `'046d'` `'ID_VENDOR_ID'` | The vendor ID of the USB device. | `'046d'` `'ID_VENDOR_FROM_DATABASE'` | USB device vendor's name, from the device database. | `'Logitech, Inc.'` `'ID_USB_INTERFACES'` | A `tuple` representing the USB device's interfaces. | `('0e0100', '0e0200', '010100', '010200')` `'DEVNAME'` | The device name or path | `'/dev/bus/usb/001/003'` `'DEVTYPE'` | Should always be `'usb_device'`. | `'usb_device'` `'ID_SERIAL'` | The serial number of the USB device. | `'92C5B92F'` Note that, depending on the device and the OS, some of this information may be incomplete or certain attributes may overlap with others. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731838012.0 usb_monitor-1.23/usb_monitor.egg-info/SOURCES.txt0000644000076500000240000000117714716340074020613 0ustar00harustaffLICENSE README.md setup.py usb_monitor.egg-info/PKG-INFO usb_monitor.egg-info/SOURCES.txt usb_monitor.egg-info/dependency_links.txt usb_monitor.egg-info/requires.txt usb_monitor.egg-info/top_level.txt usbmonitor/__init__.py usbmonitor/attributes.py usbmonitor/usbmonitor.py usbmonitor/__platform_specific_detectors/__init__.py usbmonitor/__platform_specific_detectors/_constants.py usbmonitor/__platform_specific_detectors/_darwin_usb_detector.py usbmonitor/__platform_specific_detectors/_linux_usb_detector.py usbmonitor/__platform_specific_detectors/_usb_detector_base.py usbmonitor/__platform_specific_detectors/_windows_usb_detector.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731838012.0 usb_monitor-1.23/usb_monitor.egg-info/dependency_links.txt0000644000076500000240000000000114716340074022767 0ustar00harustaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731838012.0 usb_monitor-1.23/usb_monitor.egg-info/requires.txt0000644000076500000240000000012314716340074021315 0ustar00harustaff [:platform_system == "Linux"] pyudev [:platform_system == "Windows"] pywin32 wmi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731838012.0 usb_monitor-1.23/usb_monitor.egg-info/top_level.txt0000644000076500000240000000001314716340074021445 0ustar00harustaffusbmonitor ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1731838012.465091 usb_monitor-1.23/usbmonitor/0000755000076500000240000000000014716340074015070 5ustar00harustaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717225473.0 usb_monitor-1.23/usbmonitor/__init__.py0000644000076500000240000000035714626544001017202 0ustar00harustafffrom .usbmonitor import USBMonitor # Import platform-specific detectors from .__platform_specific_detectors._windows_usb_detector import _WindowsUSBDetector from .__platform_specific_detectors._linux_usb_detector import _LinuxUSBDetector././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1731838012.4668252 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/0000755000076500000240000000000014716340074023133 5ustar00harustaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717225473.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/__init__.py0000644000076500000240000000000014626544001025226 0ustar00harustaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731837645.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/_constants.py0000644000076500000240000000640214716337315025666 0ustar00harustaff""" This file contains constant values and mappings used in the platform-specific USB device detection implementations. It includes constants for attributes, attribute separators, and regex patterns for specific Operating Systems. Additionally, it defines the default time interval for checking USB device changes. Author: Eric-Canas Date: 28-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from ..attributes import ID_MODEL_ID, ID_VENDOR, ID_MODEL, ID_VENDOR_FROM_DATABASE, ID_MODEL_FROM_DATABASE, \ DEVNAME, ID_USB_CLASS_FROM_DATABASE, ID_USB_INTERFACES, DEVTYPE, ID_VENDOR_ID, ID_SERIAL _SECONDS_BETWEEN_CHECKS = 0.5 _THREAD_JOIN_TIMEOUT_SECONDS = 5 _DEVICE_ID, _PNP_DEVICE_ID = 'DeviceID', 'PNPDeviceID' _LINUX_TO_WINDOWS_ATTRIBUTES = { ID_MODEL_ID: 'HardwareID', ID_MODEL: 'Name', ID_MODEL_FROM_DATABASE: 'Caption', ID_VENDOR_ID: 'HardwareID', ID_VENDOR: 'Name', ID_VENDOR_FROM_DATABASE: 'Manufacturer', ID_USB_INTERFACES: 'CompatibleID', ID_USB_CLASS_FROM_DATABASE: 'PNPClass', DEVNAME: _DEVICE_ID, DEVTYPE: _PNP_DEVICE_ID, ID_SERIAL: _PNP_DEVICE_ID } _LINUX_TUPLE_ATTRIBUTES_SEPARATORS = {ID_USB_INTERFACES: ':'} USB, USBSTOR, USB4, USBPRINT = 'USB', 'USBSTOR', 'USB4', 'USBPRINT' _WINDOWS_USB_REGEX_ATTRIBUTES = {ID_MODEL_ID: r'PID_([0-9A-Fa-f]{4})', ID_VENDOR_ID: r'VID_([0-9A-Fa-f]{4})', DEVTYPE: r'^(.+?)\\', ID_SERIAL: r'\\([^\\]+)$'} _WINDOWS_USB4_REGEX_ATTRIBUTES = {ID_MODEL_ID: r'PID_([0-9A-Fa-f]{4})', ID_VENDOR_ID: r'VID_([0-9A-Fa-f]{4})', DEVTYPE: r'^(.+?)\\', ID_SERIAL: r'\\([^\\]+)$'} _WINDOWS_USBPRINT_REGEX_ATTRIBUTES = {ID_MODEL_ID: r'PID_([0-9A-Fa-f]{4})', ID_VENDOR_ID: r'VID_([0-9A-Fa-f]{4})', DEVTYPE: r'^(.+?)\\', ID_SERIAL: r'\\([^\\]+)$'} _WINDOWS_USBSTOR_REGEX_ATTRIBUTES = {ID_MODEL_ID: r'PROD_([a-zA-Z0-9\_\/\.\-]{2,16})&', ID_VENDOR_ID: r'VEN_([a-zA-Z0-9\.\_\-\/]{2,8})&', DEVTYPE: r'^(.+?)\\', ID_SERIAL: r'\\([^\\]+)$'} _WINDOWS_REGEX_ATTRIBUTES_BY_DRIVER = {USB: _WINDOWS_USB_REGEX_ATTRIBUTES, USBSTOR: _WINDOWS_USBSTOR_REGEX_ATTRIBUTES, USB4: _WINDOWS_USB4_REGEX_ATTRIBUTES, USBPRINT: _WINDOWS_USBPRINT_REGEX_ATTRIBUTES} _WINDOWS_TO_LOWERCASE_ATTRIBUTES = (ID_MODEL_ID, ID_VENDOR_ID) _WINDOWS_NON_USB_DEVICES_IDS = ("ROOT_HUB20", "ROOT_HUB30", "VIRTUAL_POWER_PDO") _WINDOWS_USB_QUERY = f"SELECT {', '.join(set(_LINUX_TO_WINDOWS_ATTRIBUTES.values()))} FROM Win32_PnPEntity " \ f"WHERE {_PNP_DEVICE_ID} LIKE 'USB%'" # Darwin-specific constants _DARWIN_TO_LINUX_ATTRIBUTES = { ID_MODEL_ID: 'idProduct', ID_MODEL: 'kUSBProductString', ID_MODEL_FROM_DATABASE: 'USB Product Name', ID_VENDOR_ID: 'idVendor', ID_VENDOR: 'kUSBVendorString', ID_VENDOR_FROM_DATABASE: 'USB Vendor Name', ID_USB_INTERFACES: 'IOCFPlugInTypes', ID_USB_CLASS_FROM_DATABASE: 'bDeviceClass', DEVNAME: 'BSD Name', DEVTYPE: 'Device Speed', ID_SERIAL: 'kUSBSerialNumberString', } _DARWIN_REGEX_ATTRIBUTES = { ID_MODEL_ID: r'idProduct: ([0-9A-Fa-f]{4})', ID_VENDOR_ID: r'idVendor: ([0-9A-Fa-f]{4})' } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717231981.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/_darwin_usb_detector.py0000644000076500000240000001076114626560555027707 0ustar00harustaff""" _DarwinUSBDetector: This platform-specific implementation of the _USBDetectorBase class is designed for MacOS systems. It provides the necessary functionality to detect USB devices connected to a MacOS system and monitor changes in their connections. The class utilizes the ioreg command to interact with the MacOS system's device management subsystem. Author: Eric-Canas Date: 01-06-2024 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from __future__ import annotations import re import subprocess from warnings import warn from ..attributes import DEVTYPE, ID_VENDOR_ID, DEVNAME, DEVICE_ATTRIBUTES from ._constants import _DARWIN_TO_LINUX_ATTRIBUTES, _DARWIN_REGEX_ATTRIBUTES, _SECONDS_BETWEEN_CHECKS from ._usb_detector_base import _USBDetectorBase class _DarwinUSBDetector(_USBDetectorBase): def __init__(self, filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None = None): super(_DarwinUSBDetector, self).__init__(filter_devices=filter_devices) def get_available_devices(self) -> dict[str, dict[str, str]]: """ Returns a dictionary of the currently available devices, where the key is the device ID and the value is a dictionary of the device's information. :return: dict[str, dict[str, str]]. The key is the device ID, the value is a dictionary of the device's information. """ devices_info = self.__get_usb_devices() if self.filter_devices is not None: devices_info = self._apply_devices_filter(devices=devices_info) return devices_info def _monitor_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: """ Monitors the USB devices. This function should ALWAYS be called from a background thread. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param check_every_seconds: int | float. The number of seconds to wait between each check for changes in the USB devices. Defaults to 0.5 seconds. """ while not self._stop_thread.is_set(): self.check_changes(on_connect=on_connect, on_disconnect=on_disconnect) self._stop_thread.wait(check_every_seconds) def __get_usb_devices(self) -> dict[str, dict[str, str]]: """ Retrieves the list of USB devices using the `ioreg` command. :return: dict[str, dict[str, str]]. A dictionary of the device information. """ try: ioreg_output = subprocess.check_output(['ioreg', '-p', 'IOUSB', '-w0', '-l'], text=True) except subprocess.CalledProcessError as e: warn(f"Failed to retrieve USB devices information: {e}") return {} devices_info = {} current_device = {} device_id = None for line in ioreg_output.splitlines(): if "+-o" in line: if current_device and device_id: devices_info[device_id] = current_device current_device = {} device_id_match = re.search(r"\+-o\s+(.+?)\s+<", line) if device_id_match: device_id = device_id_match.group(1) else: device_id = None else: for attribute, darwin_attr in _DARWIN_TO_LINUX_ATTRIBUTES.items(): if darwin_attr in line: value = line.split('=')[-1].strip().strip("}").strip('"') current_device[attribute] = value for attribute, regex in _DARWIN_REGEX_ATTRIBUTES.items(): match = re.search(regex, line) if match: current_device[attribute] = match.group(1) if current_device and device_id: devices_info[device_id] = current_device # Add the device_id as the devname to the info dict for device_id, device_info in devices_info.items(): device_info[DEVNAME] = device_id return devices_info ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717877015.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/_linux_usb_detector.py0000644000076500000240000001076614631134427027556 0ustar00harustaff""" _LinuxUSBDetector: This platform-specific implementation of the _USBDetectorBase class is designed for Linux systems. It provides the necessary functionality to detect USB devices connected to a Linux system and monitor changes in their connections. The class utilizes the pyudev library to interact with the Linux system's device management subsystem (udev). Author: Eric-Canas Date: 28-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from __future__ import annotations from ._constants import _SECONDS_BETWEEN_CHECKS, _LINUX_TUPLE_ATTRIBUTES_SEPARATORS from ..attributes import ID_VENDOR_ID, DEVTYPE, DEVICE_ATTRIBUTES, DEVNAME from ._usb_detector_base import _USBDetectorBase class _LinuxUSBDetector(_USBDetectorBase): def __init__(self, filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None = None): import pyudev self.context = pyudev.Context() self.monitor = None super(_LinuxUSBDetector, self).__init__(filter_devices=filter_devices) def get_available_devices(self) -> dict[str, dict[str, str | tuple[str, ...]]]: """ Returns a dictionary of the currently available devices, where the key is the device ID and the value is a dictionary of the device's information. :return: dict[str, dict[str, str]]. The key is the device ID, the value is a dictionary of the device's information. """ usb_devices = [device for device in self.context.list_devices(subsystem='usb') if ID_VENDOR_ID in device] devices_info = {} for device in usb_devices: device_id = device[DEVNAME] device_info = {attr: device.get(attr, "") for attr in DEVICE_ATTRIBUTES} device_info = self.__generate_tuple_attributes_from_string(device_info=device_info) devices_info[device_id] = device_info if self.filter_devices is not None: devices_info = self._apply_devices_filter(devices=devices_info) return devices_info def __generate_tuple_attributes_from_string(self, device_info: dict[str, str]) -> dict[str, tuple[str]|str]: """ Generates a tuple of attributes for those attributes that are expected to be a tuple, but are stored as a string. :param device_info: dict[str, str]. The device information. :return: dict[str, tuple[str]|str]. The device information with the tuple attributes. """ for attribute, separator in _LINUX_TUPLE_ATTRIBUTES_SEPARATORS.items(): if attribute in device_info: assert isinstance(device_info[attribute], str), f"The attribute '{attribute}' is expected to be a string" # noinspection PyTypeChecker device_info[attribute] = tuple(value for value in device_info[attribute].split(separator) if value != "") return device_info def _monitor_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: import pyudev def __handle_device_event(device): action = device.action if device.get(DEVTYPE) == 'usb_device': device_id = device[DEVNAME] if action == "add" and on_connect is not None: device_info = {attr: device.get(attr, "") for attr in DEVICE_ATTRIBUTES} device_info = self.__generate_tuple_attributes_from_string(device_info=device_info) on_connect(device_id, device_info) self.last_check_devices = self.get_available_devices() elif action == "remove" and on_disconnect is not None and device_id in self.last_check_devices: device_info = self.last_check_devices[device_id].copy() on_disconnect(device_id, device_info) self.last_check_devices = self.get_available_devices() if self.monitor is None: self.monitor = pyudev.Monitor.from_netlink(self.context) self.monitor.filter_by(subsystem='usb') self._thread = pyudev.MonitorObserver(self.monitor, name="USB Monitor", callback=__handle_device_event) # Start the observer thread self._thread.start() # Keep the main thread alive, checking for changes every specified interval while not self._stop_thread.is_set(): self._stop_thread.wait(check_every_seconds) # Stop the observer thread self._thread.stop()././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717877302.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/_usb_detector_base.py0000644000076500000240000002461614631135066027330 0ustar00harustaff""" _USBDetectorBase: This abstract base class provides the core functionality for monitoring USB devices on different platforms. It defines the required methods and attributes, as well as common functionality for checking device changes, starting and stopping the monitoring process, and maintaining the state of the monitoring thread. The _USBDetectorBase class is intended to be subclassed by platform-specific implementations to provide the necessary support for USB device monitoring. Author: Eric-Canas Date: 27-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from __future__ import annotations import threading from warnings import warn from abc import ABC, abstractmethod from ._constants import _SECONDS_BETWEEN_CHECKS, _THREAD_JOIN_TIMEOUT_SECONDS class _USBDetectorBase(ABC): def __init__(self, filter_devices: list[dict[str, str]] | tuple[dict[str, str]] = None): """ Initializes the _USBDetectorBase class. :param filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None. A list of dictionaries containing the device information to filter the devices by. If None, no filtering will be done. The dictionaries must contain the same keys as the dictionaries returned by the `get_available_devices` method. For example, if you want to only monitor devices with ID_MODEL_ID = "A2B2" or "ABCD" you could pass filter_devices=({"ID_MODEL_ID": "A2B2"}, {"ID_MODEL_ID": "ABCD"}). """ self._thread = None self.filter_devices = filter_devices self._stop_thread = threading.Event() if filter_devices is not None: assert isinstance(filter_devices, (list, tuple)), f"filter_devices must be a list or a tuple of dicts " \ f"(or None). Got {type(filter_devices)}" assert all(isinstance(device, dict) for device in filter_devices), f"filter_devices must contain dicts. " \ f"Got {set(type(device) for device in filter_devices)}" self.on_start_devices = self.get_available_devices() self.last_check_devices = self.on_start_devices.copy() def changes_from_last_check(self, update_last_check_devices: bool = True) -> tuple[dict[str, str], dict[str, str]]: """ Returns a tuple of two tuples, the first containing the device IDs of the devices that were removed, the second containing the device IDs of the devices that were added. :param update_last_check_devices: bool. Whether to update the last checked devices to the current devices :return: tuple[dict[str, str], dict[str, str]]. The first tuple contains the information of the devices that were removed, the second tuple contains the information of the new devices that were added. """ current_devices, prev_devices = self.get_available_devices(), self.last_check_devices # Get the difference between the current devices and the previous ones removed_devices = {_id: _info for _id, _info in prev_devices.items() if _id not in current_devices} added_devices = {_id: _info for _id, _info in current_devices.items() if _id not in prev_devices} # Update the last checked devices to the current devices if requested if update_last_check_devices: self.last_check_devices = current_devices.copy() return removed_devices, added_devices @abstractmethod def get_available_devices(self) -> dict[str, dict[str, str | tuple[str, ...]]]: """ Returns a dictionary of the currently available devices, where the key is the device ID and the value is a dictionary of the device's information. :return: dict[str, dict[str, str|tuple[str, ...]]. The key is the device ID, the value is a dictionary of the device's information. """ raise NotImplementedError("This method must be implemented in the child class") def _apply_devices_filter(self, devices: dict[str, dict[str, str | tuple[str, ...]]]) -> dict[str, dict[str, str | tuple[str, ...]]]: """ Filters the devices by the given filters. Only devices that match all the filters in any of the dictionaries will be returned. :param devices: dict[str, dict[str, str|tuple[str, ...]]]. The devices to filter. Returned by the `get_available_devices` method. :return: dict[str, dict[str, str|tuple[str, ...]]]. The input devices that matches any of the given filters """ # Copy the dict to avoid allow iteration while modifying the original dict for device_id, device_info in devices.copy().items(): # Iterate over each filter dict for filter_dict in self.filter_devices: if all(device_info[key] == value for key, value in filter_dict.items()): break else: devices.pop(device_id) return devices def check_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, update_last_check_devices: bool = True) -> None: """ Checks for changes in the USB devices. If a device is removed, the `on_disconnect` function will be called with the device ID as the first argument and the device information as the second argument. If a device is added, the `on_connect` function with the same arguments. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param update_last_check_devices: bool. Whether to update the last checked devices to the current devices """ removed_devices, added_devices = self.changes_from_last_check(update_last_check_devices=update_last_check_devices) if on_disconnect is not None: for device_id, device_info in removed_devices.items(): on_disconnect(device_id, device_info) if on_connect is not None: for device_id, device_info in added_devices.items(): on_connect(device_id, device_info) def start_monitoring(self, on_connect: callable|None = None, on_disconnect: callable|None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: """ Starts monitoring the USB devices. This function will trigger a background thread that will check for changes in the USB devices every `check_every_seconds` seconds. If a device is removed, the `on_disconnect` function will be called with the device ID as the first argument and the device information as the second argument. If a device is added, the `on_connect` function with the same arguments. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param check_every_seconds: int | float. The number of seconds to wait between each check for changes in the USB devices. Defaults to 0.5 seconds. """ assert self._thread is None, "The USB monitor is already running" self._thread = threading.Thread(name="USB Monitor", target=self._monitor_changes, args=(on_connect, on_disconnect, check_every_seconds), daemon=True) self._thread.start() def _monitor_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: """ Monitors the USB devices. This function should ALWAYS be called from a background thread. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param check_every_seconds: int | float. The number of seconds to wait between each check for changes in the USB devices. Defaults to 0.5 seconds. """ assert self._thread is not None, "The USB monitor is not running" if self._stop_thread.is_set(): warn("USB monitor can not be started because it is already stopped. Call stop_monitoring() first", RuntimeWarning) while not self._stop_thread.is_set(): self.check_changes(on_connect=on_connect, on_disconnect=on_disconnect) self._stop_thread.wait(check_every_seconds) def stop_monitoring(self, warn_if_was_stopped: bool = True, warn_if_timeout: bool = True, timeout=_THREAD_JOIN_TIMEOUT_SECONDS) -> None: """ Stops monitoring the USB devices. :param warn_if_was_stopped: bool. Whether to warn if the USB monitor was already stopped. """ if self._thread is not None: self._stop_thread.set() self._thread.join(timeout=timeout) if warn_if_timeout and self._thread.is_alive(): warn(f"USB monitor thread did not stop in {timeout} seconds. " f"It could still be running", RuntimeWarning) self._thread = None elif warn_if_was_stopped: warn("USB monitor can not be stopped because it is not running", RuntimeWarning) self._stop_thread.clear() def __del__(self): self.stop_monitoring(warn_if_was_stopped=False)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717225473.0 usb_monitor-1.23/usbmonitor/__platform_specific_detectors/_windows_usb_detector.py0000644000076500000240000001607114626544001030101 0ustar00harustaff""" _WindowsUSBDetector: This platform-specific implementation of the _USBDetectorBase class is designed for Windows systems. It provides the necessary functionality to detect USB devices connected to a Windows system and monitor changes in their connections. The class utilizes the WMI (Windows Management Instrumentation) library to interact with the Windows system's device management subsystem. Author: Eric-Canas Date: 27-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from __future__ import annotations import re from warnings import warn from ..attributes import DEVTYPE from ._constants import _DEVICE_ID, _LINUX_TO_WINDOWS_ATTRIBUTES, _SECONDS_BETWEEN_CHECKS, \ _WINDOWS_USB_REGEX_ATTRIBUTES, \ _WINDOWS_NON_USB_DEVICES_IDS, _WINDOWS_USB_QUERY, _WINDOWS_TO_LOWERCASE_ATTRIBUTES, \ _WINDOWS_REGEX_ATTRIBUTES_BY_DRIVER from ._usb_detector_base import _USBDetectorBase class _WindowsUSBDetector(_USBDetectorBase): def __init__(self, filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None = None): self._wmi_interface = None # CONSTANTS super(_WindowsUSBDetector, self).__init__(filter_devices=filter_devices) def get_available_devices(self) -> dict[str, dict[str, str]]: """ Returns a dictionary of the currently available devices, where the key is the device ID and the value is a dictionary of the device's information. :return: dict[str, dict[str, str]]. The key is the device ID, the value is a dictionary of the device's information. """ if self._wmi_interface is None: self._wmi_interface = self.__create_wmi_interface() devices = self._wmi_interface.query(_WINDOWS_USB_QUERY) devices = {getattr(device, _DEVICE_ID): {new_name: getattr(device, attribute) for new_name, attribute in _LINUX_TO_WINDOWS_ATTRIBUTES.items()} for device in devices} devices = self.__filter_devices(devices=devices) devices = self.__finetune_incompatible_attributes(devices=devices) if self.filter_devices is not None: devices = self._apply_devices_filter(devices=devices) return devices def _monitor_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: """ Monitors the USB devices. This function should ALWAYS be called from a background thread. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param check_every_seconds: int | float. The number of seconds to wait between each check for changes in the USB devices. Defaults to 0.5 seconds. """ # If running this in a background thread, we MUST create the WMI interface inside the thread. self._wmi_interface = self.__create_wmi_interface() super(_WindowsUSBDetector, self)._monitor_changes(on_connect=on_connect, on_disconnect=on_disconnect, check_every_seconds=check_every_seconds) def __filter_devices(self, devices: dict[str, dict[str, tuple[str]|str]]) -> dict[str, dict[str, tuple[str]|str]]: """ Filters the devices to only include the ones that are USB devices. :param devices: dict[str, dict[str, str]]. The dictionary of devices to filter. :return: dict[str, dict[str, str]]. The filtered devices. """ return {device_id: device_info for device_id, device_info in devices.items() if not any(substring in device_info[DEVTYPE] for substring in _WINDOWS_NON_USB_DEVICES_IDS)} def __finetune_incompatible_attributes(self, devices: dict[str, dict[str | tuple[str, ...]]]) -> dict[str, dict[str, str]]: """ Transforms some attributes to be more similar to the Linux attributes. :param devices: dict[str, str|tuple[str,...]]. The dictionary of devices to transform. :return: dict[str, dict[str, str]]. The transformed devices. """ for device_id, device_info in devices.items(): driver_type = self.__get_driver_type_from_device_id(device_id=device_id) new_attributes = {attribute: self.__apply_regex(value=device_id, regex=regex)#device_info[attribute], regex) for attribute, regex in _WINDOWS_REGEX_ATTRIBUTES_BY_DRIVER[driver_type].items() if attribute in device_info} device_info.update(new_attributes) new_attributes = {attr: device_info[attr].upper() for attr in _WINDOWS_TO_LOWERCASE_ATTRIBUTES} device_info.update(new_attributes) return devices def __apply_regex(self, value: tuple[str] | str, regex: str) -> str: """ Apply the regex to a value, taking into account if it is a tuple or not. :param value: tuple[str] | str. The value to apply the regex to. :param regex: str. The regex to apply. :return: str. The value after applying the regex. """ if isinstance(value, str): value = (value,) values_found = [] for value in value: match = re.search(regex, value) if match is not None: values_found.append(match.group(1)) # If no value was found, return the original value if len(values_found) == 0: warn(f"Could not find a value for the regex '{regex}' in the value '{value}'") return value # Otherwise, return check there are no inconsistencies and return the value assert all(value == values_found[0] for value in values_found), "The values found are not all the same" return values_found[0] def __get_driver_type_from_device_id(self, device_id: str) -> str: """ Returns the driver type from the device ID. :param device_id: str. The device ID. :return: str. The driver type. """ device_splitted_info = device_id.split('\\') assert len(device_splitted_info) >= 1, f"The device ID '{device_id}' is not valid" driver_type = device_splitted_info[0] assert driver_type in _WINDOWS_REGEX_ATTRIBUTES_BY_DRIVER, f"The driver type '{driver_type}' is not supported " \ f"yet, please create an issue in github" return driver_type def __create_wmi_interface(self): from pythoncom import CoInitialize CoInitialize() import wmi # If running this in a background thread, we MUST create the WMI interface inside the thread. return wmi.WMI()././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1731780303.0 usb_monitor-1.23/usbmonitor/attributes.py0000644000076500000240000000206614716157317017642 0ustar00harustaff""" device_info dictionary attributes. Same attributes as Linux udev (independently of the OS where the library is running). All these attributes are the keys of the device_info dictionary returned by the `get_available_devices` method. The expected values of these attributes are all strings, except for the `ID_USB_INTERFACES` attribute, which is a tuple. Author: Eric-Canas Date: 26-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ ID_VENDOR_ID = 'ID_VENDOR_ID' ID_VENDOR = 'ID_VENDOR' ID_MODEL = 'ID_MODEL' ID_MODEL_ID = 'ID_MODEL_ID' ID_SERIAL = 'ID_SERIAL' ID_USB_INTERFACES = 'ID_USB_INTERFACES' ID_REVISION = 'ID_REVISION' ID_USB_CLASS_FROM_DATABASE = 'ID_USB_CLASS_FROM_DATABASE' ID_VENDOR_FROM_DATABASE = 'ID_VENDOR_FROM_DATABASE' ID_MODEL_FROM_DATABASE = 'ID_MODEL_FROM_DATABASE' DEVNAME = 'DEVNAME' DEVTYPE = 'DEVTYPE' DEVICE_ATTRIBUTES = (ID_MODEL_ID, ID_MODEL, ID_MODEL_FROM_DATABASE, ID_VENDOR, ID_VENDOR_ID, ID_VENDOR_FROM_DATABASE, ID_USB_INTERFACES, ID_USB_CLASS_FROM_DATABASE, DEVNAME, DEVTYPE, ID_SERIAL) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717874808.0 usb_monitor-1.23/usbmonitor/usbmonitor.py0000644000076500000240000001577314631130170017646 0ustar00harustaff""" USBMonitor: USBMonitor is an easy-to-use cross-platform library for USB device monitoring that simplifies tracking of connections, disconnections, and examination of connected device attributes on both Windows and Linux, freeing the user from platform-specific details or incompatibilities. Author: Eric-Canas Date: 24-03-2023 Email: eric@ericcanas.com Github: https://github.com/Eric-Canas """ from __future__ import annotations from .__platform_specific_detectors._constants import _SECONDS_BETWEEN_CHECKS, _THREAD_JOIN_TIMEOUT_SECONDS from warnings import warn import sys class USBMonitor: def __init__(self, filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None = None): """ Creates a new USBMonitor object. This object can be used to monitor USB devices connected to the system. :param filter_devices: list[dict[str, str]] | tuple[dict[str, str]] | None. A list of dictionaries containing the device information to filter the devices by. If None, no filtering will be done. The dictionaries must contain the same keys as the dictionaries returned by the `get_available_devices` method. For example, if you want to only monitor devices with ID_MODEL_ID = "A2B2" or "ABCD" you could pass filter_devices=({"ID_MODEL_ID": "A2B2"}, {"ID_MODEL_ID": "ABCD"}). """ if sys.platform.startswith('linux'): from .__platform_specific_detectors._linux_usb_detector import _LinuxUSBDetector self.monitor = _LinuxUSBDetector(filter_devices=filter_devices) elif sys.platform.startswith('win'): from .__platform_specific_detectors._windows_usb_detector import _WindowsUSBDetector self.monitor = _WindowsUSBDetector(filter_devices=filter_devices) elif sys.platform.startswith('darwin'): from .__platform_specific_detectors._darwin_usb_detector import _DarwinUSBDetector self.monitor = _DarwinUSBDetector(filter_devices=filter_devices) else: raise NotImplementedError(f"Your OS is not supported: {sys.platform}") def get_available_devices(self) -> dict[str, dict[str, str | tuple[str, ...]]]: """ Returns a dictionary of the currently available devices, where the key is the device ID and the value is a dictionary of the device's information. These keys IDs can be found at attributes.DEVICE_ATTRIBUTES. They do always correspond with the default Linux IDs (independently of the OS where the library is running). :return: dict[str, dict[str, str|tuple[str, ...]]]. The key is the device ID, the value is a dictionary of the device's information. """ return self.monitor.get_available_devices() def check_changes(self, on_connect: callable | None = None, on_disconnect: callable | None = None, update_last_check_devices: bool = True) -> None: """ Checks for changes in the USB devices. If a device is removed, the `on_disconnect` function will be called with the device ID as the first argument and the device information as the second argument. If a device is added, the `on_connect` function with the same arguments. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param update_last_check_devices: bool. Whether to update the last checked devices to the current devices """ self.monitor.check_changes(on_connect=on_connect, on_disconnect=on_disconnect, update_last_check_devices=update_last_check_devices) def changes_from_last_check(self, update_last_check_devices: bool = True) -> tuple[dict[str, str], dict[str, str]]: """ Returns a tuple of two tuples, the first containing the device IDs of the devices that were removed, the second containing the device IDs of the devices that were added. :param update_last_check_devices: bool. Whether to update the last checked devices to the current devices :return: tuple[dict[str, str], dict[str, str]]. The first tuple contains the information of the devices that were removed, the second tuple contains the information of the new devices that were added. """ return self.monitor.changes_from_last_check(update_last_check_devices=update_last_check_devices) def start_monitoring(self, on_connect: callable|None = None, on_disconnect: callable|None = None, check_every_seconds: int | float = _SECONDS_BETWEEN_CHECKS) -> None: """ Starts monitoring the USB devices. This function will trigger a background thread that will check for changes in the USB devices every `check_every_seconds` seconds. If a device is removed, the `on_disconnect` function will be called with the device ID as the first argument and the device information as the second argument. If a device is added, the `on_connect` function with the same arguments. :param on_connect: callable | None. The function to call when a device is added. It is expected to receive two arguments, the device ID and the device information. on_connect(device_id: str, device_info: dict[str, str]) :param on_disconnect: callable | None. The function to call when a device is removed. It is expected to receive two arguments, the device ID and the device information. on_disconnect(device_id: str, device_info: dict[str, str]) :param check_every_seconds: int | float. The number of seconds to wait between each check for changes in the USB devices. Defaults to 0.5 seconds. """ if on_connect is None and on_disconnect is None: warn("You are starting the monitor without any callback functions. This won't notice anything " "when a device is connected or disconnected.") self.monitor.start_monitoring(on_connect=on_connect, on_disconnect=on_disconnect, check_every_seconds=check_every_seconds) def stop_monitoring(self, timeout=_THREAD_JOIN_TIMEOUT_SECONDS) -> None: """ Stops monitoring the USB devices. This function will stop the background thread that was checking for changes in the USB devices. """ self.monitor.stop_monitoring(timeout=timeout) # When requesting a function that does not exist, it will be redirected to the monitor def __getattr__(self, item): if hasattr(self.monitor, item): return getattr(self.monitor, item) else: raise AttributeError(f"USBMonitor has no attribute '{item}'")