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. |