"""TrueNAS Controller.""" from __future__ import annotations import logging from datetime import datetime, timedelta from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, ) from .api import TrueNASAPI from .apiparser import parse_api, utc_from_timestamp from .const import DOMAIN _LOGGER = logging.getLogger(__name__) # --------------------------- # TrueNASControllerData # --------------------------- class TrueNASCoordinator(DataUpdateCoordinator[None]): """TrueNASCoordinator Class.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): """Initialize TrueNASCoordinator.""" self.hass = hass self.config_entry: ConfigEntry = config_entry super().__init__( self.hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=60), ) self.name = config_entry.data[CONF_NAME] self.host = config_entry.data[CONF_HOST] self.ds = { "interface": {}, "disk": {}, "pool": {}, "dataset": {}, "system_info": {}, "service": {}, "jail": {}, "vm": {}, "cloudsync": {}, "replication": {}, "snapshottask": {}, "app": {}, } self.api = TrueNASAPI( hass, config_entry.data[CONF_HOST], config_entry.data[CONF_API_KEY], config_entry.data[CONF_SSL], config_entry.data[CONF_VERIFY_SSL], ) self._systemstats_errored = [] self.datasets_hass_device_id = None self.last_updatecheck_update = datetime(1970, 1, 1) self._is_scale = False self._is_virtual = False self._version_major = 0 self._version_minor = 0 # --------------------------- # connected # --------------------------- def connected(self) -> bool: """Return connected state.""" return self.api.connected() # --------------------------- # _async_update_data # --------------------------- async def _async_update_data(self): """Update TrueNAS data.""" await self.hass.async_add_executor_job(self.get_systeminfo) if self.api.connected(): await self.hass.async_add_executor_job(self.get_systemstats) if self.api.connected(): await self.hass.async_add_executor_job(self.get_service) if self.api.connected(): await self.hass.async_add_executor_job(self.get_disk) if self.api.connected(): await self.hass.async_add_executor_job(self.get_dataset) if self.api.connected(): await self.hass.async_add_executor_job(self.get_pool) if self.api.connected(): await self.hass.async_add_executor_job(self.get_jail) if self.api.connected(): await self.hass.async_add_executor_job(self.get_vm) if self.api.connected(): await self.hass.async_add_executor_job(self.get_cloudsync) if self.api.connected(): await self.hass.async_add_executor_job(self.get_replication) if self.api.connected(): await self.hass.async_add_executor_job(self.get_snapshottask) if self.api.connected(): await self.hass.async_add_executor_job(self.get_app) delta = datetime.now().replace(microsecond=0) - self.last_updatecheck_update if self.api.connected() and delta.total_seconds() > 60 * 60 * 12: await self.hass.async_add_executor_job(self.get_updatecheck) self.last_updatecheck_update = datetime.now().replace(microsecond=0) if not self.api.connected(): raise UpdateFailed("TrueNas Disconnected") return self.ds # --------------------------- # get_systeminfo # --------------------------- def get_systeminfo(self) -> None: """Get system info from TrueNAS.""" self.ds["system_info"] = parse_api( data=self.ds["system_info"], source=self.api.query("system/info"), vals=[ {"name": "version", "default": "unknown"}, {"name": "hostname", "default": "unknown"}, {"name": "uptime_seconds", "default": 0}, {"name": "system_serial", "default": "unknown"}, {"name": "system_product", "default": "unknown"}, {"name": "system_manufacturer", "default": "unknown"}, ], ensure_vals=[ {"name": "uptimeEpoch", "default": 0}, {"name": "cpu_temperature", "default": 0.0}, {"name": "load_shortterm", "default": 0.0}, {"name": "load_midterm", "default": 0.0}, {"name": "load_longterm", "default": 0.0}, {"name": "cpu_interrupt", "default": 0.0}, {"name": "cpu_system", "default": 0.0}, {"name": "cpu_user", "default": 0.0}, {"name": "cpu_nice", "default": 0.0}, {"name": "cpu_idle", "default": 0.0}, {"name": "cpu_usage", "default": 0.0}, {"name": "cache_size-arc_value", "default": 0.0}, {"name": "memory-used_value", "default": 0.0}, {"name": "memory-free_value", "default": 0.0}, {"name": "memory-cached_value", "default": 0.0}, {"name": "memory-buffered_value", "default": 0.0}, {"name": "memory-total_value", "default": 0.0}, {"name": "memory-usage_percent", "default": 0}, {"name": "update_available", "type": "bool", "default": False}, {"name": "update_progress", "default": 0}, {"name": "update_jobid", "default": 0}, {"name": "update_state", "default": "unknown"}, ], ) if not self.api.connected(): return if not self.ds["system_info"]["update_available"]: self.ds["system_info"]["update_version"] = self.ds["system_info"]["version"] if self.ds["system_info"]["update_jobid"]: self.ds["system_info"] = parse_api( data=self.ds["system_info"], source=self.api.query( "core/get_jobs", method="get", params={"id": self.ds["system_info"]["update_jobid"]}, ), vals=[ { "name": "update_progress", "source": "progress/percent", "default": 0, }, { "name": "update_state", "source": "state", "default": "unknown", }, ], ) if not self.api.connected(): return if ( self.ds["system_info"]["update_state"] != "RUNNING" or not self.ds["system_info"]["update_available"] ): self.ds["system_info"]["update_progress"] = 0 self.ds["system_info"]["update_jobid"] = 0 self.ds["system_info"]["update_state"] = "unknown" self._is_scale = bool( self.ds["system_info"]["version"].startswith("TrueNAS-SCALE-") ) if self._is_scale: if not self._version_major: self._version_major = int( self.ds["system_info"] .get("version") .removeprefix("TrueNAS-") .removeprefix("SCALE-") .split(".")[0] ) if not self._version_minor: self._version_minor = int( self.ds["system_info"] .get("version") .removeprefix("TrueNAS-") .removeprefix("SCALE-") .split(".")[1] ) self._is_virtual = self.ds["system_info"]["system_manufacturer"] in [ "QEMU", "VMware, Inc.", ] or self.ds["system_info"]["system_product"] in [ "VirtualBox", ] if self.ds["system_info"]["uptime_seconds"] > 0: now = datetime.now().replace(microsecond=0) uptime_tm = datetime.timestamp( now - timedelta(seconds=int(self.ds["system_info"]["uptime_seconds"])) ) self.ds["system_info"]["uptimeEpoch"] = utc_from_timestamp(uptime_tm) self.ds["interface"] = parse_api( data=self.ds["interface"], source=self.api.query("interface"), key="id", vals=[ {"name": "id", "default": "unknown"}, {"name": "name", "default": "unknown"}, {"name": "description", "default": "unknown"}, {"name": "mtu", "default": "unknown"}, { "name": "link_state", "source": "state/link_state", "default": "unknown", }, { "name": "active_media_type", "source": "state/active_media_type", "default": "unknown", }, { "name": "active_media_subtype", "source": "state/active_media_subtype", "default": "unknown", }, { "name": "link_address", "source": "state/link_address", "default": "unknown", }, ], ensure_vals=[ {"name": "rx", "default": 0}, {"name": "tx", "default": 0}, ], ) # --------------------------- # get_updatecheck # --------------------------- def get_updatecheck(self) -> None: self.ds["system_info"] = parse_api( data=self.ds["system_info"], source=self.api.query("update/check_available", method="post"), vals=[ { "name": "update_status", "source": "status", "default": "unknown", }, { "name": "update_version", "source": "version", "default": "unknown", }, ], ) if not self.api.connected(): return if ( self.ds["system_info"]["update_version"] == "unknown" and self.ds["system_info"]["version"] ): self.ds["system_info"]["update_version"] = self.ds["system_info"]["version"] self.ds["system_info"]["update_available"] = ( self.ds["system_info"]["update_status"] == "AVAILABLE" ) # --------------------------- # get_systemstats # --------------------------- def get_systemstats(self) -> None: """Get system statistics.""" tmp_params = { "graphs": [ {"name": "load"}, {"name": "cputemp"}, {"name": "cpu"}, {"name": "arcsize"}, {"name": "memory"}, ], "reporting_query": { "start": "now-90s", "end": "now-30s", "aggregate": True, }, } if self._is_scale and self._version_major == 23: tmp_params = { "graphs": [ {"name": "load"}, {"name": "cputemp"}, {"name": "cpu"}, {"name": "arcsize"}, {"name": "memory"}, ], "reporting_query_netdata": { "start": "-90", "end": "-30", "aggregate": True, }, } elif self._is_scale and self._version_major >= 24: tmp_params = { "graphs": [ {"name": "load"}, {"name": "cputemp"}, {"name": "cpu"}, {"name": "arcsize"}, {"name": "memory"}, ], "reporting_query": { "start": "-90", "end": "-30", "aggregate": True, }, } for uid, vals in self.ds["interface"].items(): tmp_params["graphs"].append({"name": "interface", "identifier": uid}) if self._is_virtual: tmp_params["graphs"].remove({"name": "cputemp"}) for tmp in tmp_params["graphs"]: if tmp["name"] in self._systemstats_errored: tmp_params["graphs"].remove(tmp) if not tmp_params["graphs"]: return reporting_path = "reporting/get_data" if self._is_scale and self._version_major >= 23: reporting_path = "reporting/netdata_get_data" tmp_graph = self.api.query( reporting_path, method="post", params=tmp_params, ) if not isinstance(tmp_graph, list): if self.api.error == 500: for tmp in tmp_params["graphs"]: tmp_params2 = { "graphs": [tmp], "reporting_query": { "start": "now-90s", "end": "now-30s", "aggregate": True, }, } if self._is_scale and self._version_major == 23: tmp_params2 = { "graphs": [tmp], "reporting_query_netdata": { "start": "-90", "end": "-30", "aggregate": True, }, } elif self._is_scale and self._version_major >= 24: tmp_params2 = { "graphs": [tmp], "reporting_query": { "start": "-90", "end": "-30", "aggregate": "true", }, } tmp2 = self.api.query( reporting_path, method="post", params=tmp_params2, ) if not isinstance(tmp2, list) and self.api.error == 500: self._systemstats_errored.append(tmp["name"]) _LOGGER.warning( "TrueNAS %s fetching following graphs failed, check your NAS: %s", self.host, self._systemstats_errored, ) self.get_systemstats() return for i in range(len(tmp_graph)): if "name" not in tmp_graph[i]: continue # CPU temperature if tmp_graph[i]["name"] == "cputemp": if "aggregations" in tmp_graph[i]: if self._is_scale and self._version_major >= 23: self.ds["system_info"]["cpu_temperature"] = round( max(tmp_graph[i]["aggregations"]["mean"].values()), 2 ) else: self.ds["system_info"]["cpu_temperature"] = round( max( list(filter(None, tmp_graph[i]["aggregations"]["mean"])) ), 1, ) else: self.ds["system_info"]["cpu_temperature"] = 0.0 # CPU load if tmp_graph[i]["name"] == "load": tmp_arr = ("load_shortterm", "load_midterm", "load_longterm") if self._is_scale and self._version_major >= 23: tmp_arr = ("shortterm", "midterm", "longterm") self._systemstats_process(tmp_arr, tmp_graph[i], "load") # CPU usage if tmp_graph[i]["name"] == "cpu": tmp_arr = ("interrupt", "system", "user", "nice", "idle") if self._is_scale and self._version_major >= 23: tmp_arr = ("softirq", "system", "user", "nice", "iowait", "idle") self._systemstats_process(tmp_arr, tmp_graph[i], "cpu") self.ds["system_info"]["cpu_usage"] = round( self.ds["system_info"]["cpu_system"] + self.ds["system_info"]["cpu_user"], 2, ) # Interface if tmp_graph[i]["name"] == "interface": tmp_etc = tmp_graph[i]["identifier"] if tmp_etc in self.ds["interface"]: # 12->13 API change tmp_graph[i]["legend"] = [ tmp.replace("if_octets_", "") for tmp in tmp_graph[i]["legend"] ] if self._is_scale and self._version_major >= 23: tmp_graph[i]["legend"] = [ tmp.replace("received", "rx") for tmp in tmp_graph[i]["legend"] ] tmp_graph[i]["legend"] = [ tmp.replace("sent", "tx") for tmp in tmp_graph[i]["legend"] ] tmp_graph[i]["aggregations"]["mean"] = { k.replace("received", "rx"): v for k, v in tmp_graph[i]["aggregations"]["mean"].items() } tmp_graph[i]["aggregations"]["mean"] = { k.replace("sent", "tx"): v for k, v in tmp_graph[i]["aggregations"]["mean"].items() } tmp_arr = ("rx", "tx") if "aggregations" in tmp_graph[i]: for e in range(len(tmp_graph[i]["legend"])): tmp_var = tmp_graph[i]["legend"][e] if tmp_var in tmp_arr: if self._is_scale and self._version_major >= 23: tmp_val = ( tmp_graph[i]["aggregations"]["mean"][tmp_var] or 0.0 if tmp_var in tmp_graph[i]["aggregations"]["mean"] else 0.0 ) self.ds["interface"][tmp_etc][tmp_var] = round( (tmp_val * 0.12207), 2 ) else: tmp_val = ( tmp_graph[i]["aggregations"]["mean"][e] or 0.0 if e in tmp_graph[i]["aggregations"]["mean"] else 0.0 ) self.ds["interface"][tmp_etc][tmp_var] = round( (tmp_val / 1024), 2 ) else: for tmp_load in tmp_arr: self.ds["interface"][tmp_etc][tmp_load] = 0.0 # memory if tmp_graph[i]["name"] == "memory": tmp_arr = ( "memory-used_value", "memory-free_value", "memory-cached_value", "memory-buffered_value", ) if self._is_scale and self._version_major >= 23: tmp_arr = ( "free", "used", "cached", "buffers", ) self._systemstats_process(tmp_arr, tmp_graph[i], "memory") self.ds["system_info"]["memory-total_value"] = round( self.ds["system_info"]["memory-used_value"] + self.ds["system_info"]["memory-free_value"] + self.ds["system_info"]["cache_size-arc_value"] ) if self.ds["system_info"]["memory-total_value"] > 0: self.ds["system_info"]["memory-usage_percent"] = round( 100 * ( float(self.ds["system_info"]["memory-total_value"]) - float(self.ds["system_info"]["memory-free_value"]) ) / float(self.ds["system_info"]["memory-total_value"]) ) # arcsize if tmp_graph[i]["name"] == "arcsize": tmp_arr = "cache_size-arc_value" if self._is_scale and self._version_major >= 23: tmp_arr = "arc_size" self._systemstats_process(tmp_arr, tmp_graph[i], "arcsize") # --------------------------- # _systemstats_process # --------------------------- def _systemstats_process(self, arr, graph, t) -> None: if "aggregations" in graph: for e in range(len(graph["legend"])): tmp_var = graph["legend"][e] if tmp_var in arr: if self._is_scale and self._version_major >= 23: e = tmp_var tmp_val = graph["aggregations"]["mean"][e] or 0.0 if t == "memory": if self._is_scale and self._version_major >= 23: if tmp_var == "free": self.ds["system_info"]["memory-free_value"] = round( tmp_val * 1024 * 1024 ) elif tmp_var == "used": self.ds["system_info"]["memory-used_value"] = round( tmp_val * 1024 * 1024 ) elif tmp_var == "cached": self.ds["system_info"]["memory-cached_value"] = round( tmp_val * 1024 * 1024 ) elif tmp_var == "buffers": self.ds["system_info"]["memory-buffered_value"] = round( tmp_val * 1024 * 1024 ) else: self.ds["system_info"][tmp_var] = tmp_val elif t == "cpu": self.ds["system_info"][f"cpu_{tmp_var}"] = round(tmp_val, 2) elif t == "load": if self._is_scale and self._version_major >= 23: self.ds["system_info"][f"load_{tmp_var}"] = round( tmp_val, 2 ) else: self.ds["system_info"][tmp_var] = round(tmp_val, 2) elif t == "arcsize": if self._is_scale and self._version_major >= 23: tmp_val = tmp_val * 1024 * 1024 self.ds["system_info"]["cache_size-arc_value"] = round( tmp_val, 2 ) else: self.ds["system_info"][tmp_var] = round(tmp_val, 2) else: self.ds["system_info"][tmp_var] = round(tmp_val, 2) else: for tmp_load in arr: if t == "cpu": self.ds["system_info"][f"cpu_{tmp_load}"] = 0.0 else: self.ds["system_info"][tmp_load] = 0.0 # --------------------------- # get_service # --------------------------- def get_service(self) -> None: """Get service info from TrueNAS.""" self.ds["service"] = parse_api( data=self.ds["service"], source=self.api.query("service"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "service", "default": "unknown"}, {"name": "enable", "type": "bool", "default": False}, {"name": "state", "default": "unknown"}, ], ensure_vals=[ {"name": "running", "type": "bool", "default": False}, ], ) for uid, vals in self.ds["service"].items(): self.ds["service"][uid]["running"] = vals["state"] == "RUNNING" # --------------------------- # get_pool # --------------------------- def get_pool(self) -> None: """Get pools from TrueNAS.""" self.ds["pool"] = parse_api( data=self.ds["pool"], source=self.api.query("pool"), key="guid", vals=[ {"name": "guid", "default": 0}, {"name": "id", "default": 0}, {"name": "name", "default": "unknown"}, {"name": "path", "default": "unknown"}, {"name": "status", "default": "unknown"}, {"name": "healthy", "type": "bool", "default": False}, {"name": "is_decrypted", "type": "bool", "default": False}, { "name": "autotrim", "source": "autotrim/parsed", "type": "bool", "default": False, }, { "name": "scan_function", "source": "scan/function", "default": "unknown", }, {"name": "scrub_state", "source": "scan/state", "default": "unknown"}, { "name": "scrub_start", "source": "scan/start_time/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "scrub_end", "source": "scan/end_time/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "scrub_secs_left", "source": "scan/total_secs_left", "default": 0, }, ], ensure_vals=[ {"name": "available_gib", "default": 0.0}, {"name": "total_gib", "default": 0.0}, {"name": "usage", "default": 0.0}, ], ) self.ds["pool"] = parse_api( data=self.ds["pool"], source=self.api.query("boot/get_state"), key="name", vals=[ {"name": "guid", "default": "boot-pool"}, {"name": "id", "default": "boot-pool"}, {"name": "name", "default": "unknown"}, {"name": "path", "default": "unknown"}, {"name": "status", "default": "unknown"}, {"name": "healthy", "type": "bool", "default": False}, {"name": "is_decrypted", "type": "bool", "default": False}, { "name": "autotrim", "source": "autotrim/parsed", "type": "bool", "default": False, }, {"name": "root_dataset"}, { "name": "root_dataset_available", "source": "root_dataset/properties/available/parsed", "default": 0, }, { "name": "root_dataset_used", "source": "root_dataset/properties/used/parsed", "default": 0, }, { "name": "scan_function", "source": "scan/function", "default": "unknown", }, {"name": "scrub_state", "source": "scan/state", "default": "unknown"}, { "name": "scrub_start", "source": "scan/start_time/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "scrub_end", "source": "scan/end_time/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "scrub_secs_left", "source": "scan/total_secs_left", "default": 0, }, {"name": "allocated", "default": 0}, {"name": "free", "default": 0}, ], ensure_vals=[ {"name": "available_gib", "default": 0.0}, {"name": "total_gib", "default": 0.0}, {"name": "usage", "default": 0.0}, ], ) # Process pools tmp_dataset_available = {} tmp_dataset_total = {} for uid, vals in self.ds["dataset"].items(): tmp_dataset_available[self.ds["dataset"][uid]["mountpoint"]] = vals[ "available" ] tmp_dataset_total[self.ds["dataset"][uid]["mountpoint"]] = ( vals["available"] + vals["used"] ) for uid, vals in self.ds["pool"].items(): if vals["path"] in tmp_dataset_available: self.ds["pool"][uid]["available_gib"] = tmp_dataset_available[ vals["path"] ] if vals["path"] in tmp_dataset_total: self.ds["pool"][uid]["total_gib"] = tmp_dataset_total[vals["path"]] if vals["name"] in ["boot-pool", "freenas-boot"]: if self._is_scale and self._version_major >= 23: self.ds["pool"][uid]["available_gib"] = vals["free"] self.ds["pool"][uid]["total_gib"] = vals["free"] + vals["allocated"] else: self.ds["pool"][uid]["available_gib"] = vals[ "root_dataset_available" ] self.ds["pool"][uid]["total_gib"] = ( vals["root_dataset_available"] + vals["root_dataset_used"] ) self.ds["pool"][uid].pop("root_dataset") if self.ds["pool"][uid]["total_gib"] > 0: self.ds["pool"][uid]["usage"] = round( ( self.ds["pool"][uid]["available_gib"] / self.ds["pool"][uid]["total_gib"] ) * 100 ) else: self.ds["pool"][uid]["usage"] = 0 # --------------------------- # get_dataset # --------------------------- def get_dataset(self) -> None: """Get datasets from TrueNAS.""" self.ds["dataset"] = parse_api( data={}, source=self.api.query("pool/dataset"), key="id", vals=[ {"name": "id", "default": "unknown"}, {"name": "type", "default": "unknown"}, {"name": "name", "default": "unknown"}, {"name": "pool", "default": "unknown"}, {"name": "mountpoint", "default": "unknown"}, {"name": "comments", "source": "comments/parsed", "default": ""}, { "name": "deduplication", "source": "deduplication/parsed", "type": "bool", "default": False, }, { "name": "atime", "source": "atime/parsed", "type": "bool", "default": False, }, { "name": "casesensitivity", "source": "casesensitivity/parsed", "default": "unknown", }, {"name": "checksum", "source": "checksum/parsed", "default": "unknown"}, { "name": "exec", "source": "exec/parsed", "type": "bool", "default": False, }, {"name": "sync", "source": "sync/parsed", "default": "unknown"}, { "name": "compression", "source": "compression/parsed", "default": "unknown", }, { "name": "compressratio", "source": "compressratio/parsed", "default": "unknown", }, {"name": "quota", "source": "quota/parsed", "default": "unknown"}, {"name": "copies", "source": "copies/parsed", "default": 0}, { "name": "readonly", "source": "readonly/parsed", "type": "bool", "default": False, }, {"name": "recordsize", "source": "recordsize/parsed", "default": 0}, { "name": "encryption_algorithm", "source": "encryption_algorithm/parsed", "default": "unknown", }, {"name": "used", "source": "used/parsed", "default": 0}, {"name": "available", "source": "available/parsed", "default": 0}, ], ensure_vals=[ {"name": "used_gb", "default": 0}, ], ) for uid, vals in self.ds["dataset"].items(): self.ds["dataset"][uid]["used_gb"] = vals["used"] if len(self.ds["dataset"]) == 0: return # entities_to_be_removed = [] # if not self.datasets_hass_device_id: # device_registry = dr.async_get(self.hass) # for device in device_registry.devices.values(): # if ( # self.config_entry.entry_id in device.config_entries # and device.name.endswith("Datasets") # ): # self.datasets_hass_device_id = device.id # _LOGGER.debug(f"datasets device: {device.name}") # # if not self.datasets_hass_device_id: # return # # _LOGGER.debug(f"datasets_hass_device_id: {self.datasets_hass_device_id}") # entity_registry = er.async_get(self.hass) # entity_entries = async_entries_for_config_entry( # entity_registry, self.config_entry.entry_id # ) # for entity in entity_entries: # if ( # entity.device_id == self.datasets_hass_device_id # and entity.unique_id.removeprefix(f"{self.name.lower()}-dataset-") # not in map( # lambda x: str.replace(x, "/", "_"), # map(str.lower, self.ds["dataset"].keys()), # ) # ): # _LOGGER.debug(f"dataset to be removed: {entity.unique_id}") # entities_to_be_removed.append(entity.entity_id) # # for entity_id in entities_to_be_removed: # entity_registry.async_remove(entity_id) # --------------------------- # get_disk # --------------------------- def get_disk(self) -> None: """Get disks from TrueNAS.""" self.ds["disk"] = parse_api( data=self.ds["disk"], source=self.api.query("disk"), key="identifier", vals=[ {"name": "name", "default": "unknown"}, {"name": "devname", "default": "unknown"}, {"name": "serial", "default": "unknown"}, {"name": "size", "default": "unknown"}, {"name": "hddstandby", "default": "unknown"}, {"name": "hddstandby_force", "type": "bool", "default": False}, {"name": "advpowermgmt", "default": "unknown"}, {"name": "acousticlevel", "default": "unknown"}, {"name": "togglesmart", "type": "bool", "default": False}, {"name": "model", "default": "unknown"}, {"name": "rotationrate", "default": "unknown"}, {"name": "type", "default": "unknown"}, {"name": "zfs_guid", "default": "unknown"}, {"name": "identifier", "default": "unknown"}, ], ensure_vals=[ {"name": "temperature", "default": 0}, ], ) # Get disk temperatures temps = self.api.query( "disk/temperatures", method="post", params={"names": []}, ) if temps: for uid, vals in self.ds["disk"].items(): if vals["name"] in temps: # looks for devname here self.ds["disk"][uid]["temperature"] = temps[vals["name"]] # return devname temp to uid disk # I feel like this will break in the future when TrueNAS updates to a more sensible system. Currently their own long term stats are broken by the changing devnames. # --------------------------- # get_jail # --------------------------- def get_jail(self) -> None: """Get jails from TrueNAS.""" if self._is_scale: return self.ds["jail"] = parse_api( data=self.ds["jail"], source=self.api.query("jail"), key="id", vals=[ {"name": "id", "default": "unknown"}, {"name": "comment", "default": "unknown"}, {"name": "host_hostname", "default": "unknown"}, {"name": "jail_zfs_dataset", "default": "unknown"}, {"name": "last_started", "default": "unknown"}, {"name": "ip4_addr", "default": "unknown"}, {"name": "ip6_addr", "default": "unknown"}, {"name": "release", "default": "unknown"}, {"name": "state", "type": "bool", "default": False}, {"name": "type", "default": "unknown"}, {"name": "plugin_name", "default": "unknown"}, ], ) # --------------------------- # get_vm # --------------------------- def get_vm(self) -> None: """Get VMs from TrueNAS.""" self.ds["vm"] = parse_api( data=self.ds["vm"], source=self.api.query("vm"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "name", "default": "unknown"}, {"name": "description", "default": "unknown"}, {"name": "vcpus", "default": 0}, {"name": "memory", "default": 0}, {"name": "autostart", "type": "bool", "default": False}, {"name": "cores", "default": 0}, {"name": "threads", "default": 0}, {"name": "state", "source": "status/state", "default": "unknown"}, ], ensure_vals=[ {"name": "running", "type": "bool", "default": False}, ], ) for uid, vals in self.ds["vm"].items(): self.ds["vm"][uid]["running"] = vals["state"] == "RUNNING" # --------------------------- # get_cloudsync # --------------------------- def get_cloudsync(self) -> None: """Get cloudsync from TrueNAS.""" self.ds["cloudsync"] = parse_api( data=self.ds["cloudsync"], source=self.api.query("cloudsync"), key="id", vals=[ {"name": "id", "default": "unknown"}, {"name": "description", "default": "unknown"}, {"name": "direction", "default": "unknown"}, {"name": "path", "default": "unknown"}, {"name": "enabled", "type": "bool", "default": False}, {"name": "transfer_mode", "default": "unknown"}, {"name": "snapshot", "type": "bool", "default": False}, {"name": "state", "source": "job/state", "default": "unknown"}, { "name": "time_started", "source": "job/time_started/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "time_finished", "source": "job/time_finished/$date", "default": 0, "convert": "utc_from_timestamp", }, {"name": "job_percent", "source": "job/progress/percent", "default": 0}, { "name": "job_description", "source": "job/progress/description", "default": "unknown", }, ], ) # --------------------------- # get_replication # --------------------------- def get_replication(self) -> None: """Get replication from TrueNAS.""" self.ds["replication"] = parse_api( data=self.ds["replication"], source=self.api.query("replication"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "name", "default": "unknown"}, {"name": "source_datasets", "default": "unknown"}, {"name": "target_dataset", "default": "unknown"}, {"name": "recursive", "type": "bool", "default": False}, {"name": "enabled", "type": "bool", "default": False}, {"name": "direction", "default": "unknown"}, {"name": "transport", "default": "unknown"}, {"name": "auto", "type": "bool", "default": False}, {"name": "retention_policy", "default": "unknown"}, {"name": "state", "source": "job/state", "default": "unknown"}, { "name": "time_started", "source": "job/time_started/$date", "default": 0, "convert": "utc_from_timestamp", }, { "name": "time_finished", "source": "job/time_finished/$date", "default": 0, "convert": "utc_from_timestamp", }, {"name": "job_percent", "source": "job/progress/percent", "default": 0}, { "name": "job_description", "source": "job/progress/description", "default": "unknown", }, ], ) # --------------------------- # get_snapshottask # --------------------------- def get_snapshottask(self) -> None: """Get replication from TrueNAS.""" self.ds["snapshottask"] = parse_api( data=self.ds["snapshottask"], source=self.api.query("pool/snapshottask"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "dataset", "default": "unknown"}, {"name": "recursive", "type": "bool", "default": False}, {"name": "lifetime_value", "default": 0}, {"name": "lifetime_unit", "default": "unknown"}, {"name": "enabled", "type": "bool", "default": False}, {"name": "naming_schema", "default": "unknown"}, {"name": "allow_empty", "type": "bool", "default": False}, {"name": "vmware_sync", "type": "bool", "default": False}, {"name": "state", "source": "state/state", "default": "unknown"}, { "name": "datetime", "source": "state/datetime/$date", "default": 0, "convert": "utc_from_timestamp", }, ], ) # --------------------------- # get_app # --------------------------- def get_app(self) -> None: """Get Apps from TrueNAS.""" if not self._is_scale: return if self._version_major <= 23 or ( self._version_major == 24 and self._version_minor < 10 ): self.ds["app"] = parse_api( data=self.ds["app"], source=self.api.query("chart/release"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "name", "default": "unknown"}, {"name": "human_version", "type": "bool", "default": False}, {"name": "update_available", "type": "bool", "default": False}, { "name": "image_updates_available", "source": "container_images_update_available", "default": "unknown", }, {"name": "portal", "source": "portals/open", "default": "unknown"}, {"name": "status", "default": "unknown"}, ], ensure_vals=[ {"name": "running", "type": "bool", "default": False}, ], ) for uid, vals in self.ds["app"].items(): self.ds["app"][uid]["running"] = vals["status"] == "ACTIVE" else: self.ds["app"] = parse_api( data=self.ds["app"], source=self.api.query("app"), key="id", vals=[ {"name": "id", "default": 0}, {"name": "name", "default": "unknown"}, {"name": "human_version", "default": "unknown"}, { "name": "update_available", "source": "upgrade_available", "type": "bool", "default": False, }, { "name": "image_updates_available", "type": "bool", "default": False, }, { "name": "portal", "source": "portals/Web UI", "default": "unknown", }, {"name": "state", "default": "unknown"}, ], ensure_vals=[ {"name": "running", "type": "bool", "default": False}, ], ) for uid, vals in self.ds["app"].items(): self.ds["app"][uid]["running"] = vals["state"] == "RUNNING"