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.