diff --git a/custom_components/idrac_power/__init__.py b/custom_components/idrac_power/__init__.py old mode 100644 new mode 100755 index 1bada69..8bbd92f --- a/custom_components/idrac_power/__init__.py +++ b/custom_components/idrac_power/__init__.py @@ -31,19 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, Platform.SENSOR - ) - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, Platform.BINARY_SENSOR - ) - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, Platform.BUTTON + hass.config_entries.async_forward_entry_setups( + entry, [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.BUTTON] ) ) return True diff --git a/custom_components/idrac_power/binary_sensor.py b/custom_components/idrac_power/binary_sensor.py old mode 100644 new mode 100755 index 16e23cf..3492d42 --- a/custom_components/idrac_power/binary_sensor.py +++ b/custom_components/idrac_power/binary_sensor.py @@ -8,10 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.exceptions import PlatformNotReady from .const import (DOMAIN, DATA_IDRAC_REST_CLIENT, JSON_MODEL, JSON_MANUFACTURER, JSON_SERIAL_NUMBER, DATA_IDRAC_FIRMWARE, DATA_IDRAC_INFO) -from .idrac_rest import IdracRest +from .idrac_rest import IdracRest, CannotConnect, RedfishConfig _LOGGER = logging.getLogger(__name__) @@ -25,22 +26,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e """Add iDrac power sensor entry""" rest_client = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_REST_CLIENT] - if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: - info = await hass.async_add_executor_job(target=rest_client.get_device_info) - if not info: - _LOGGER.error(f"Could not set up: couldn't reach device.") - return - - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info - else: - info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] - - firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) - if not firmware_version: - if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: - firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] - else: - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version + try: + if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: + info = await hass.async_add_executor_job(target=rest_client.get_device_info) + if not info: + raise PlatformNotReady(f"Could not set up: device didn't return anything.") + + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info + else: + info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] + + firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) + if not firmware_version: + if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: + firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] + else: + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version + except (CannotConnect, RedfishConfig) as e: + raise PlatformNotReady(str(e)) from e + model = info[JSON_MODEL] name = model diff --git a/custom_components/idrac_power/button.py b/custom_components/idrac_power/button.py old mode 100644 new mode 100755 index 77e990d..215f0d2 --- a/custom_components/idrac_power/button.py +++ b/custom_components/idrac_power/button.py @@ -7,10 +7,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.exceptions import PlatformNotReady from .const import (DOMAIN, DATA_IDRAC_REST_CLIENT, JSON_MODEL, JSON_MANUFACTURER, JSON_SERIAL_NUMBER, DATA_IDRAC_INFO, DATA_IDRAC_FIRMWARE) -from .idrac_rest import IdracRest +from .idrac_rest import IdracRest, CannotConnect, RedfishConfig _LOGGER = logging.getLogger(__name__) @@ -19,22 +20,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e """Add iDrac power sensor entry""" rest_client = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_REST_CLIENT] - if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: - info = await hass.async_add_executor_job(target=rest_client.get_device_info) - if not info: - _LOGGER.error(f"Could not set up: couldn't reach device.") - return - - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info - else: - info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] - - firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) - if not firmware_version: - if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: - firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] - else: - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version + try: + if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: + info = await hass.async_add_executor_job(target=rest_client.get_device_info) + if not info: + raise PlatformNotReady(f"Could not set up: device didn't return anything.") + + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info + else: + info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] + + firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) + if not firmware_version: + if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: + firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] + else: + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version + except (CannotConnect, RedfishConfig) as e: + raise PlatformNotReady(str(e)) from e model = info[JSON_MODEL] name = model diff --git a/custom_components/idrac_power/config_flow.py b/custom_components/idrac_power/config_flow.py old mode 100644 new mode 100755 diff --git a/custom_components/idrac_power/const.py b/custom_components/idrac_power/const.py old mode 100644 new mode 100755 diff --git a/custom_components/idrac_power/idrac_rest.py b/custom_components/idrac_power/idrac_rest.py old mode 100644 new mode 100755 index 48a5541..1ab8004 --- a/custom_components/idrac_power/idrac_rest.py +++ b/custom_components/idrac_power/idrac_rest.py @@ -5,6 +5,7 @@ import urllib3 from homeassistant.exceptions import HomeAssistantError from requests import Response +from requests.exceptions import RequestException, JSONDecodeError urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -28,7 +29,11 @@ def handle_error(result): raise InvalidAuth() if result.status_code == 404: - error = result.json()['error'] + try: + error = result.json()['error'] + except JSONDecodeError: + # start of iDRAC can cause 404 error, ignore it + raise CannotConnect(f"iDRAC responed with 404, but no JSON present:\n{result.text}") if error['code'] == 'Base.1.0.GeneralError' and 'RedFish attribute is disabled' in \ error['@Message.ExtendedInfo'][0]['Message']: raise RedfishConfig() @@ -54,9 +59,8 @@ def __init__(self, host, username, password, interval): def get_device_info(self) -> dict | None: try: result = self.get_path(drac_chassis_path) - except ConnectionError: - _LOGGER.warning(f"Could not get device info from {self.host}") - return None + except RequestException: + raise CannotConnect(f"Cannot connect to {self.host}") handle_error(result) @@ -71,9 +75,8 @@ def get_device_info(self) -> dict | None: def get_firmware_version(self) -> str | None: try: result = self.get_path(drac_managers_path) - except ConnectionError: - _LOGGER.warning(f"Could not get firmware version of {self.host}") - return None + except RequestException: + raise CannotConnect(f"Could not get firmware version of {self.host}") handle_error(result) @@ -81,15 +84,14 @@ def get_firmware_version(self) -> str | None: return manager_results[JSON_FIRMWARE_VERSION] def get_path(self, path): - return requests.get(protocol + self.host + path, auth=self.auth, verify=False) + return requests.get(protocol + self.host + path, auth=self.auth, verify=False, timeout=300) def power_on(self) -> Response | None: try: result = requests.post(protocol + self.host + drac_powerON_path, auth=self.auth, verify=False, - json={"ResetType": "On"}) - except ConnectionError as e: - _LOGGER.error(f"Could power on {self.host}: {e}") - return None + json={"ResetType": "On"}, timeout=300) + except RequestException as e: + raise CannotConnect(f"Could not power on {self.host}: {e}") json = result.json() if result.status_code == 401: @@ -101,7 +103,7 @@ def power_on(self) -> Response | None: error['@Message.ExtendedInfo'][0]['Message']: raise RedfishConfig() if "error" in json: - _LOGGER.error("Idrac power on failed: %s", json["error"]["@Message.ExtendedInfo"][0]["Message"]) + _LOGGER.error("iDRAC power on failed: %s", json["error"]["@Message.ExtendedInfo"][0]["Message"]) return result @@ -120,8 +122,8 @@ def update_thermals(self) -> dict: handle_error(req) new_thermals = req.json() - except ConnectionError as e: - _LOGGER.warning(f"Couldn't update {self.host} thermals: {e}") + except (RequestException, RedfishConfig, CannotConnect) as e: + _LOGGER.debug(f"Couldn't update {self.host} thermals: {e}") new_thermals = None if new_thermals != self.thermal_values: @@ -141,9 +143,9 @@ def update_status(self): except: new_status = None - except ConnectionError as e: - _LOGGER.warning(f"Couldn't update {self.host} status: {e}") - new_status = False + except (RequestException, RedfishConfig, CannotConnect) as e: + _LOGGER.debug(f"Couldn't update {self.host} status: {e}") + new_status = None if new_status != self.status: self.status = new_status @@ -155,8 +157,8 @@ def update_power_usage(self): result = self.get_path(drac_powercontrol_path) handle_error(result) power_values = result.json() - except ConnectionError as e: - _LOGGER.warning(f"Couldn't update {self.host} thermals: {e}") + except (RequestException, RedfishConfig, CannotConnect) as e: + _LOGGER.debug(f"Couldn't update {self.host} power usage: {e}") for callback in self.callback_power_usage: callback(None) return diff --git a/custom_components/idrac_power/manifest.json b/custom_components/idrac_power/manifest.json old mode 100644 new mode 100755 diff --git a/custom_components/idrac_power/sensor.py b/custom_components/idrac_power/sensor.py old mode 100644 new mode 100755 index e1bc6b4..6522b77 --- a/custom_components/idrac_power/sensor.py +++ b/custom_components/idrac_power/sensor.py @@ -8,10 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.exceptions import PlatformNotReady from .const import (DOMAIN, DATA_IDRAC_REST_CLIENT, JSON_MODEL, JSON_MANUFACTURER, JSON_SERIAL_NUMBER, DATA_IDRAC_INFO, DATA_IDRAC_FIRMWARE, DATA_IDRAC_THERMAL) -from .idrac_rest import IdracRest +from .idrac_rest import IdracRest, CannotConnect, RedfishConfig _LOGGER = logging.getLogger(__name__) @@ -27,30 +28,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e _LOGGER.debug(f"Getting the REST client for {entry.entry_id}") - if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: - info = await hass.async_add_executor_job(target=rest_client.get_device_info) - if not info: - _LOGGER.error(f"Could not set up: couldn't reach device.") - return - - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info - else: - info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] - - firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) - if not firmware_version: - if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: - firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] - else: - hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version - - thermal_info = await hass.async_add_executor_job(target=rest_client.update_thermals) - if not thermal_info: - if DATA_IDRAC_THERMAL in hass.data[DOMAIN][entry.entry_id]: - thermal_info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_THERMAL] + try: + if DATA_IDRAC_INFO not in hass.data[DOMAIN][entry.entry_id]: + info = await hass.async_add_executor_job(target=rest_client.get_device_info) + if not info: + raise PlatformNotReady(f"Could not set up: device didn't return anything.") + + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = info + else: + info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] + + firmware_version = await hass.async_add_executor_job(target=rest_client.get_firmware_version) + if not firmware_version: + if DATA_IDRAC_FIRMWARE in hass.data[DOMAIN][entry.entry_id]: + firmware_version = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_FIRMWARE] else: - _LOGGER.error(f"Could not set up: couldn't get thermal info.") - return + hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_INFO] = firmware_version + + thermal_info = await hass.async_add_executor_job(target=rest_client.update_thermals) + if not thermal_info: + if DATA_IDRAC_THERMAL in hass.data[DOMAIN][entry.entry_id]: + thermal_info = hass.data[DOMAIN][entry.entry_id][DATA_IDRAC_THERMAL] + else: + raise PlatformNotReady(f"Could not set up: couldn't get thermal info.") + except (CannotConnect, RedfishConfig) as e: + raise PlatformNotReady(str(e)) from e model = info[JSON_MODEL] name = model @@ -68,18 +70,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e _LOGGER.debug(f"Adding new devices to device info {('serial', serial)}") - entities = [IdracCurrentPowerSensor(hass, rest_client, device_info, f"{serial}_{name}_current", name)] + entities = [IdracCurrentPowerSensor(hass, rest_client, device_info, f"{serial}_{name}_power", name)] for i, fan in enumerate(thermal_info['Fans']): + member_id = fan['MemberId'] _LOGGER.info("Adding fan %s : %s", i, fan["FanName"]) - entities.append(IdracFanSensor(hass, rest_client, device_info, f"{serial}_{name}_fan_{i}", - f"{name} {fan['FanName']}", i + entities.append(IdracFanSensor(hass, rest_client, device_info, f"{serial}_{name}_fan_{member_id}", + f"{name} {fan['FanName']}", member_id )) for i, temp in enumerate(thermal_info['Temperatures']): + member_id = temp['MemberId'] _LOGGER.info("Adding temp %s : %s", i, temp["Name"]) - entities.append(IdracTempSensor(hass, rest_client, device_info, f"{serial}_{name}_temp_{i}", - f"{name} {temp['Name']}", i + entities.append(IdracTempSensor(hass, rest_client, device_info, f"{serial}_{name}_temp_{member_id}", + f"{name} {temp['Name']}", member_id )) async_add_entities(entities) @@ -91,9 +95,23 @@ async def refresh_sensors_task(): await asyncio.sleep(rest_client.interval) async def update_all(): - await hass.async_add_executor_job(rest_client.update_thermals) - await hass.async_add_executor_job(rest_client.update_status) - await hass.async_add_executor_job(rest_client.update_power_usage) + try: + await hass.async_add_executor_job(rest_client.update_thermals) + except Exception as e: + # ignore exceptions, just log the error + _LOGGER.warning(f"Updating {name} thermals sensors failed:\n{e}") + + try: + await hass.async_add_executor_job(rest_client.update_status) + except Exception as e: + # ignore exceptions, just log the error + _LOGGER.warning(f"Updating {name} status sensor failed:\n{e}") + + try: + await hass.async_add_executor_job(rest_client.update_power_usage) + except Exception as e: + # ignore exceptions, just log the error + _LOGGER.warning(f"Updating {name} power usage failed:\n{e}") await update_all() @@ -134,7 +152,7 @@ def update_value(self, new_value: int | None): class IdracFanSensor(SensorEntity): - def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, index): + def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, member_id): self.hass = hass self.rest = rest @@ -151,13 +169,16 @@ def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, index): self._attr_has_entity_name = True self._attr_native_value = None - self.index = index + self.member_id = member_id self.rest.register_callback_thermals(self.update_value) def update_value(self, thermal: dict | None): if thermal: - self._attr_native_value = thermal['Fans'][self.index]['Reading'] + for fan in thermal['Fans']: + if fan['MemberId'] == self.member_id: + self._attr_native_value = fan['Reading'] + break self._attr_available = True else: self._attr_available = False @@ -165,7 +186,7 @@ def update_value(self, thermal: dict | None): class IdracTempSensor(SensorEntity): - def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, index): + def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, member_id): self.hass = hass self.rest = rest @@ -182,13 +203,16 @@ def __init__(self, hass, rest: IdracRest, device_info, unique_id, name, index): self._attr_unique_id = unique_id self._attr_has_entity_name = True self._attr_native_value = None - self.index = index + self.member_id = member_id self.rest.register_callback_thermals(self.update_value) def update_value(self, thermal: dict | None): if thermal: - self._attr_native_value = thermal['Temperatures'][self.index]['ReadingCelsius'] + for temp in thermal['Temperatures']: + if temp['MemberId'] == self.member_id: + self._attr_native_value = temp['ReadingCelsius'] + break self._attr_available = True else: self._attr_available = False diff --git a/custom_components/idrac_power/strings.json b/custom_components/idrac_power/strings.json old mode 100644 new mode 100755