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
7.3 KiB
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:
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
# 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
# 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
# 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
# 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:
- Successful hostname retrieval
- Empty hostname in response
- Missing hostname in response
- Invalid JSON response
- Non-200 status code
- Connection error
- Timeout error
- Custom timeout parameter
- Device name parameter
- 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:
-
Code Reuse: Eliminates duplicated code for retrieving a device's hostname, reducing the codebase size and complexity.
-
Consistency: Ensures consistent error handling and logging across the codebase, making the behavior more predictable and easier to understand.
-
Maintainability: Makes it easier to update the hostname retrieval logic in one place, rather than having to update multiple locations.
-
Readability: Makes the code more concise and easier to understand, as the hostname retrieval logic is now encapsulated in a well-named function.
-
Flexibility: Provides options for customizing timeout and logging level, making the function more versatile for different use cases.
-
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.