1 """The nut component."""
3 from __future__
import annotations
5 from dataclasses
import dataclass
6 from datetime
import timedelta
8 from typing
import TYPE_CHECKING
10 from aionut
import AIONUTClient, NUTError, NUTLoginError
21 EVENT_HOMEASSISTANT_STOP,
29 DEFAULT_SCAN_INTERVAL,
31 INTEGRATION_SUPPORTED_COMMANDS,
35 NUT_FAKE_SERIAL = [
"unknown",
"blank"]
37 _LOGGER = logging.getLogger(__name__)
39 type NutConfigEntry = ConfigEntry[NutRuntimeData]
44 """Runtime data definition."""
46 coordinator: DataUpdateCoordinator
49 user_available_commands: set[str]
53 """Set up Network UPS Tools (NUT) from a config entry."""
57 if CONF_RESOURCES
in entry.options:
58 new_data = {**entry.data, CONF_RESOURCES: entry.options[CONF_RESOURCES]}
59 new_options = {k: v
for k, v
in entry.options.items()
if k != CONF_RESOURCES}
60 hass.config_entries.async_update_entry(
61 entry, data=new_data, options=new_options
65 host = config[CONF_HOST]
66 port = config[CONF_PORT]
68 alias = config.get(CONF_ALIAS)
69 username = config.get(CONF_USERNAME)
70 password = config.get(CONF_PASSWORD)
71 scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
73 data =
PyNUTData(host, port, alias, username, password)
75 entry.async_on_unload(data.async_shutdown)
77 async
def async_update_data() -> dict[str, str]:
78 """Fetch data from NUT."""
80 return await data.async_update()
81 except NUTLoginError
as err:
82 raise ConfigEntryAuthFailed
from err
83 except NUTError
as err:
84 raise UpdateFailed(f
"Error fetching UPS state: {err}")
from err
90 name=
"NUT resource status",
91 update_method=async_update_data,
92 update_interval=
timedelta(seconds=scan_interval),
97 await coordinator.async_config_entry_first_refresh()
101 entry.async_on_unload(
102 hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_shutdown)
104 status = coordinator.data
106 _LOGGER.debug(
"NUT Sensors Available: %s", status)
108 entry.async_on_unload(entry.add_update_listener(_async_update_listener))
110 if unique_id
is None:
111 unique_id = entry.entry_id
113 if username
is not None and password
is not None:
114 user_available_commands = {
115 device_supported_command
116 for device_supported_command
in await data.async_list_commands()
or {}
117 if device_supported_command
in INTEGRATION_SUPPORTED_COMMANDS
120 user_available_commands = set()
123 coordinator, data, unique_id, user_available_commands
126 device_registry = dr.async_get(hass)
127 device_registry.async_get_or_create(
128 config_entry_id=entry.entry_id,
129 identifiers={(DOMAIN, unique_id)},
130 name=data.name.title(),
131 manufacturer=data.device_info.manufacturer,
132 model=data.device_info.model,
133 model_id=data.device_info.model_id,
134 sw_version=data.device_info.firmware,
135 serial_number=data.device_info.serial,
136 suggested_area=data.device_info.device_location,
139 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
145 """Unload a config entry."""
146 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
150 """Handle options update."""
151 await hass.config_entries.async_reload(entry.entry_id)
155 """Find the best manufacturer value from the status."""
157 status.get(
"device.mfr")
158 or status.get(
"ups.mfr")
159 or status.get(
"ups.vendorid")
160 or status.get(
"driver.version.data")
165 """Find the best model value from the status."""
167 status.get(
"device.model")
168 or status.get(
"ups.model")
169 or status.get(
"ups.productid")
174 """Find the best firmware value from the status."""
175 return status.get(
"ups.firmware")
or status.get(
"ups.firmware.aux")
179 """Find the best serial value from the status."""
180 serial = status.get(
"device.serial")
or status.get(
"ups.serial")
182 serial.lower()
in NUT_FAKE_SERIAL
or serial.count(
"0") == len(serial.strip())
189 """Find the best unique id value from the status."""
200 unique_id_group.append(manufacturer)
202 unique_id_group.append(model)
204 unique_id_group.append(serial)
205 return "_".join(unique_id_group)
210 """Device information for NUT."""
212 manufacturer: str |
None =
None
213 model: str |
None =
None
214 model_id: str |
None =
None
215 firmware: str |
None =
None
216 serial: str |
None =
None
217 device_location: str |
None =
None
221 """Stores the data retrieved from NUT.
223 For each entity to use, acts as the single point responsible for fetching
224 updates from the server.
232 username: str |
None,
233 password: str |
None,
234 persistent: bool =
True,
236 """Initialize the data object."""
241 self.
_client_client = AIONUTClient(self.
_host_host, port, username, password, 5, persistent)
242 self.
ups_listups_list: dict[str, str] |
None =
None
243 self.
_status_status: dict[str, str] |
None =
None
244 self.
_device_info_device_info: NUTDeviceInfo |
None =
None
247 def status(self) -> dict[str, str] | None:
248 """Get latest update if throttle allows. Return status."""
253 """Return the name of the ups."""
254 return self.
_alias_alias
or f
"Nut-{self._host}"
258 """Return the device info for the ups."""
262 """Get the ups alias from NUT."""
263 if not (ups_list := await self.
_client_client.list_ups()):
264 _LOGGER.error(
"Empty list while getting NUT ups aliases")
267 return list(ups_list)[0]
270 """Get the ups device info from NUT."""
276 model_id: str |
None = self.
_status_status.
get(
"device.part")
279 device_location: str |
None = self.
_status_status.
get(
"device.location")
281 manufacturer, model, model_id, firmware, serial, device_location
285 """Get the ups status from NUT."""
286 if self.
_alias_alias
is None:
289 assert self.
_alias_alias
is not None
290 return await self.
_client_client.list_vars(self.
_alias_alias)
293 """Fetch the latest status from NUT."""
300 """Invoke instant command in UPS."""
302 assert self.
_alias_alias
is not None
306 except NUTError
as err:
308 f
"Error running command {command_name}, {err}"
312 """Fetch the list of supported commands."""
314 assert self.
_alias_alias
is not None
317 return await self.
_client_client.list_commands(self.
_alias_alias)
318 except NUTError
as err:
319 _LOGGER.error(
"Error retrieving supported commands %s", err)
324 """Shutdown the client connection."""
dict[str, str] async_update(self)
None __init__(self, str host, int port, str|None alias, str|None username, str|None password, bool persistent=True)
str|None _async_get_alias(self)
NUTDeviceInfo|None _get_device_info(self)
set[str]|None async_list_commands(self)
NUTDeviceInfo device_info(self)
dict[str, str] _async_get_status(self)
None async_shutdown(self, Event|None _=None)
dict[str, str]|None status(self)
None async_run_command(self, str command_name)
web.Response get(self, web.Request request, str config_key)
str|None _serial_from_status(dict[str, str] status)
str|None _manufacturer_from_status(dict[str, str] status)
None _async_update_listener(HomeAssistant hass, ConfigEntry entry)
str|None _firmware_from_status(dict[str, str] status)
bool async_setup_entry(HomeAssistant hass, NutConfigEntry entry)
str|None _unique_id_from_status(dict[str, str] status)
str|None _model_from_status(dict[str, str] status)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None run_command(argparse.Namespace args)