1. Restructured configuration: Moved config_other and console to top level 2. Added common _match_pattern function for regex pattern matching 3. Implemented Unifi Hostname bug fix in is_hostname_unknown 4. Created common get_device_hostname function to eliminate code duplication 5. Added comprehensive test scripts for all new functionality 6. Added detailed documentation for all changes
174 lines
7.3 KiB
Markdown
174 lines
7.3 KiB
Markdown
# Common Function for Device Hostname Retrieval
|
|
|
|
## Issue Description
|
|
|
|
The TasmotaManager codebase had multiple locations that retrieved a device's hostname from a Tasmota device using similar code patterns. This duplication made the code harder to maintain and increased the risk of inconsistencies if one implementation was updated but not the others.
|
|
|
|
## Solution
|
|
|
|
A common function `get_device_hostname` was implemented to centralize the hostname retrieval logic, eliminating code duplication and ensuring consistent error handling and logging across the codebase.
|
|
|
|
## Changes Made
|
|
|
|
### 1. Common Function Implementation
|
|
|
|
A new function `get_device_hostname` was added to the TasmotaManager class:
|
|
|
|
```python
|
|
def get_device_hostname(self, ip: str, device_name: str = None, timeout: int = 5, log_level: str = 'debug') -> tuple:
|
|
"""Retrieve the hostname from a Tasmota device.
|
|
|
|
This function makes an HTTP request to a Tasmota device to retrieve its self-reported
|
|
hostname using the Status 5 command. It handles error conditions and provides
|
|
consistent logging.
|
|
|
|
Args:
|
|
ip: The IP address of the device
|
|
device_name: Optional name of the device for logging purposes
|
|
timeout: Timeout for the HTTP request in seconds (default: 5)
|
|
log_level: The logging level to use ('debug', 'info', 'warning', 'error'). Default is 'debug'.
|
|
|
|
Returns:
|
|
tuple: (hostname, success)
|
|
- hostname: The device's self-reported hostname, or empty string if not found
|
|
- success: Boolean indicating whether the hostname was successfully retrieved
|
|
"""
|
|
# Set up logging based on the specified level
|
|
log_func = getattr(self.logger, log_level)
|
|
|
|
# Use device_name in logs if provided, otherwise use IP
|
|
device_id = device_name if device_name else ip
|
|
|
|
hostname = ""
|
|
success = False
|
|
|
|
try:
|
|
# Log attempt to retrieve hostname
|
|
log_func(f"Retrieving hostname for {device_id} at {ip}")
|
|
|
|
# Make HTTP request to the device
|
|
url = f"http://{ip}/cm?cmnd=Status%205"
|
|
response = requests.get(url, timeout=timeout)
|
|
|
|
# Check if response is successful
|
|
if response.status_code == 200:
|
|
try:
|
|
# Parse JSON response
|
|
status_data = response.json()
|
|
|
|
# Extract hostname from response
|
|
hostname = status_data.get('StatusNET', {}).get('Hostname', '')
|
|
|
|
if hostname:
|
|
log_func(f"Successfully retrieved hostname for {device_id}: {hostname}")
|
|
success = True
|
|
else:
|
|
log_func(f"No hostname found in response for {device_id}")
|
|
except ValueError:
|
|
log_func(f"Failed to parse JSON response from {device_id}")
|
|
else:
|
|
log_func(f"Failed to get hostname for {device_id}: HTTP {response.status_code}")
|
|
except requests.exceptions.RequestException as e:
|
|
log_func(f"Error retrieving hostname for {device_id}: {str(e)}")
|
|
|
|
return hostname, success
|
|
```
|
|
|
|
### 2. Updated Locations
|
|
|
|
Four locations in the codebase were updated to use the new common function:
|
|
|
|
#### a. `is_hostname_unknown` Function
|
|
|
|
```python
|
|
# Get the device's self-reported hostname using the common function
|
|
device_reported_hostname, success = self.get_device_hostname(ip, hostname, timeout=5, log_level='debug')
|
|
|
|
if success:
|
|
# Check if the self-reported hostname also matches unknown patterns
|
|
device_hostname_matches_unknown = False
|
|
for pattern in patterns:
|
|
if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False):
|
|
device_hostname_matches_unknown = True
|
|
self.logger.debug(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}")
|
|
break
|
|
```
|
|
|
|
#### b. `get_tasmota_devices` Method
|
|
|
|
```python
|
|
# Get the device's self-reported hostname using the common function
|
|
device_reported_hostname, success = self.get_device_hostname(device_ip, device_name, timeout=5, log_level='debug')
|
|
|
|
if success:
|
|
# Check if the self-reported hostname also matches unknown patterns
|
|
device_hostname_matches_unknown = False
|
|
for pattern in unknown_patterns:
|
|
if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False):
|
|
device_hostname_matches_unknown = True
|
|
self.logger.debug(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}")
|
|
break
|
|
```
|
|
|
|
#### c. `process_single_device` Method
|
|
|
|
```python
|
|
# Get the device's self-reported hostname using the common function
|
|
device_reported_hostname, success = self.get_device_hostname(device_ip, device_name, timeout=5, log_level='info')
|
|
|
|
if success and device_reported_hostname:
|
|
# Check if the self-reported hostname also matches unknown patterns
|
|
device_hostname_matches_unknown = False
|
|
for pattern in unknown_patterns:
|
|
if self._match_pattern(device_reported_hostname.lower(), pattern, match_entire_string=False):
|
|
device_hostname_matches_unknown = True
|
|
self.logger.info(f"Device's self-reported hostname '{device_reported_hostname}' matches unknown pattern: {pattern}")
|
|
break
|
|
```
|
|
|
|
#### d. Device Details Collection
|
|
|
|
```python
|
|
# Get Status 5 for network info using the common function
|
|
hostname, hostname_success = self.get_device_hostname(ip, name, timeout=5, log_level='info')
|
|
|
|
# Create a network_data structure for backward compatibility
|
|
network_data = {"StatusNET": {"Hostname": hostname if hostname_success else "Unknown"}}
|
|
```
|
|
|
|
### 3. Comprehensive Testing
|
|
|
|
A test file `test_get_device_hostname.py` was created to verify that the `get_device_hostname` function works correctly. The tests cover:
|
|
|
|
1. Successful hostname retrieval
|
|
2. Empty hostname in response
|
|
3. Missing hostname in response
|
|
4. Invalid JSON response
|
|
5. Non-200 status code
|
|
6. Connection error
|
|
7. Timeout error
|
|
8. Custom timeout parameter
|
|
9. Device name parameter
|
|
10. Log level parameter
|
|
|
|
All tests pass, confirming that the function handles all scenarios correctly.
|
|
|
|
## Benefits
|
|
|
|
The implementation of the common `get_device_hostname` function provides several benefits:
|
|
|
|
1. **Code Reuse**: Eliminates duplicated code for retrieving a device's hostname, reducing the codebase size and complexity.
|
|
|
|
2. **Consistency**: Ensures consistent error handling and logging across the codebase, making the behavior more predictable and easier to understand.
|
|
|
|
3. **Maintainability**: Makes it easier to update the hostname retrieval logic in one place, rather than having to update multiple locations.
|
|
|
|
4. **Readability**: Makes the code more concise and easier to understand, as the hostname retrieval logic is now encapsulated in a well-named function.
|
|
|
|
5. **Flexibility**: Provides options for customizing timeout and logging level, making the function more versatile for different use cases.
|
|
|
|
6. **Reliability**: Comprehensive testing ensures that the function works correctly in all scenarios, including error conditions.
|
|
|
|
## Conclusion
|
|
|
|
The implementation of the common `get_device_hostname` function has successfully eliminated code duplication, improved maintainability, and ensured consistent error handling and logging across the codebase. The function is well-tested and provides a flexible, reliable way to retrieve a device's hostname from a Tasmota device. |