Initial Commit
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""The TrueNAS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import TrueNASCoordinator
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# update_listener
|
||||
# ---------------------------
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_setup_entry
|
||||
# ---------------------------
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up TrueNAS config entry."""
|
||||
coordinator = TrueNASCoordinator(hass, config_entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_reload_entry
|
||||
# ---------------------------
|
||||
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_unload_entry
|
||||
# ---------------------------
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload TrueNAS config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
):
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
||||
"""TrueNAS API."""
|
||||
|
||||
from logging import getLogger
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
from requests import get as requests_get, post as requests_post
|
||||
from voluptuous import Optional
|
||||
from urllib3 import disable_warnings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
disable_warnings(InsecureRequestWarning)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASAPI
|
||||
# ---------------------------
|
||||
class TrueNASAPI(object):
|
||||
"""Handle all communication with TrueNAS."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
host: str,
|
||||
api_key: str,
|
||||
use_ssl: bool = False,
|
||||
verify_ssl: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the TrueNAS API."""
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._use_ssl = use_ssl
|
||||
self._api_key = api_key
|
||||
self._protocol = "https" if self._use_ssl else "http"
|
||||
self._ssl_verify = verify_ssl
|
||||
if not self._use_ssl:
|
||||
self._ssl_verify = True
|
||||
self._url = f"{self._protocol}://{self._host}/api/v2.0/"
|
||||
|
||||
self.lock = Lock()
|
||||
self._connected = False
|
||||
self._error = ""
|
||||
|
||||
# ---------------------------
|
||||
# connected
|
||||
# ---------------------------
|
||||
def connected(self) -> bool:
|
||||
"""Return connected boolean."""
|
||||
return self._connected
|
||||
|
||||
# ---------------------------
|
||||
# connection_test
|
||||
# ---------------------------
|
||||
def connection_test(self) -> tuple:
|
||||
"""Test connection."""
|
||||
self.query("pool")
|
||||
|
||||
return self._connected, self._error
|
||||
|
||||
# ---------------------------
|
||||
# query
|
||||
# ---------------------------
|
||||
def query(
|
||||
self, service: str, method: str = "get", params: dict[str, Any] | None = {}
|
||||
) -> Optional(list):
|
||||
"""Retrieve data from TrueNAS."""
|
||||
self.lock.acquire()
|
||||
error = False
|
||||
try:
|
||||
_LOGGER.debug(
|
||||
"TrueNAS %s query: %s, %s, %s",
|
||||
self._host,
|
||||
service,
|
||||
method,
|
||||
params,
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
}
|
||||
if method == "get":
|
||||
response = requests_get(
|
||||
f"{self._url}{service}",
|
||||
headers=headers,
|
||||
params=params,
|
||||
verify=self._ssl_verify,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
elif method == "post":
|
||||
response = requests_post(
|
||||
f"{self._url}{service}",
|
||||
headers=headers,
|
||||
json=params,
|
||||
verify=self._ssl_verify,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
_LOGGER.debug("TrueNAS %s query response: %s", self._host, data)
|
||||
else:
|
||||
error = True
|
||||
except Exception:
|
||||
error = True
|
||||
|
||||
if error:
|
||||
try:
|
||||
errorcode = response.status_code
|
||||
except Exception:
|
||||
errorcode = "no_response"
|
||||
|
||||
_LOGGER.warning(
|
||||
'TrueNAS %s unable to fetch data "%s" (%s)',
|
||||
self._host,
|
||||
service,
|
||||
errorcode,
|
||||
)
|
||||
|
||||
if (
|
||||
errorcode != 500
|
||||
and service != "reporting/get_data"
|
||||
and service != "reporting/netdata_get_data"
|
||||
):
|
||||
self._connected = False
|
||||
|
||||
self._error = errorcode
|
||||
self.lock.release()
|
||||
return None
|
||||
|
||||
self._connected = True
|
||||
self._error = ""
|
||||
self.lock.release()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
"""Return error."""
|
||||
return self._error
|
||||
@@ -0,0 +1,353 @@
|
||||
"""API parser for JSON APIs."""
|
||||
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
from pytz import utc
|
||||
from voluptuous import Optional
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
|
||||
from .const import TO_REDACT
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# utc_from_timestamp
|
||||
# ---------------------------
|
||||
def utc_from_timestamp(timestamp: float) -> datetime:
|
||||
"""Return a UTC time from a timestamp."""
|
||||
return utc.localize(datetime.utcfromtimestamp(timestamp))
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# from_entry
|
||||
# ---------------------------
|
||||
def from_entry(entry, param, default="") -> str:
|
||||
"""Validate and return str value an API dict."""
|
||||
if "/" in param:
|
||||
for tmp_param in param.split("/"):
|
||||
if isinstance(entry, dict) and tmp_param in entry:
|
||||
entry = entry[tmp_param]
|
||||
else:
|
||||
return default
|
||||
|
||||
ret = entry
|
||||
elif param in entry:
|
||||
ret = entry[param]
|
||||
else:
|
||||
return default
|
||||
|
||||
if default != "":
|
||||
if isinstance(ret, str):
|
||||
ret = str(ret)
|
||||
elif isinstance(ret, int):
|
||||
ret = int(ret)
|
||||
elif isinstance(ret, float):
|
||||
ret = round(float(ret), 2)
|
||||
|
||||
return ret[:255] if isinstance(ret, str) and len(ret) > 255 else ret
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# from_entry_bool
|
||||
# ---------------------------
|
||||
def from_entry_bool(entry, param, default=False, reverse=False) -> bool:
|
||||
"""Validate and return a bool value from an API dict."""
|
||||
if "/" in param:
|
||||
for tmp_param in param.split("/"):
|
||||
if isinstance(entry, dict) and tmp_param in entry:
|
||||
entry = entry[tmp_param]
|
||||
else:
|
||||
return default
|
||||
|
||||
ret = entry
|
||||
elif param in entry:
|
||||
ret = entry[param]
|
||||
else:
|
||||
return default
|
||||
|
||||
if isinstance(ret, str):
|
||||
if ret in ("on", "On", "ON", "yes", "Yes", "YES", "up", "Up", "UP"):
|
||||
ret = True
|
||||
elif ret in ("off", "Off", "OFF", "no", "No", "NO", "down", "Down", "DOWN"):
|
||||
ret = False
|
||||
|
||||
if not isinstance(ret, bool):
|
||||
ret = default
|
||||
|
||||
return not ret if reverse else ret
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# parse_api
|
||||
# ---------------------------
|
||||
def parse_api(
|
||||
data=None,
|
||||
source=None,
|
||||
key=None,
|
||||
key_secondary=None,
|
||||
key_search=None,
|
||||
vals=None,
|
||||
val_proc=None,
|
||||
ensure_vals=None,
|
||||
only=None,
|
||||
skip=None,
|
||||
) -> dict:
|
||||
"""Get data from API."""
|
||||
debug = _LOGGER.getEffectiveLevel() == 10
|
||||
if type(source) == dict:
|
||||
tmp = source
|
||||
source = [tmp]
|
||||
|
||||
if not source:
|
||||
if not key and not key_search:
|
||||
data = fill_defaults(data, vals)
|
||||
return data
|
||||
|
||||
if debug:
|
||||
_LOGGER.debug("Processing source %s", async_redact_data(source, TO_REDACT))
|
||||
|
||||
keymap = generate_keymap(data, key_search)
|
||||
for entry in source:
|
||||
if only and not matches_only(entry, only):
|
||||
continue
|
||||
|
||||
if skip and can_skip(entry, skip):
|
||||
continue
|
||||
|
||||
uid = None
|
||||
if key or key_search:
|
||||
uid = get_uid(entry, key, key_secondary, key_search, keymap)
|
||||
if not uid:
|
||||
continue
|
||||
|
||||
if uid not in data:
|
||||
data[uid] = {}
|
||||
|
||||
if debug:
|
||||
_LOGGER.debug("Processing entry %s", async_redact_data(entry, TO_REDACT))
|
||||
|
||||
if vals:
|
||||
data = fill_vals(data, entry, uid, vals)
|
||||
|
||||
if ensure_vals:
|
||||
data = fill_ensure_vals(data, uid, ensure_vals)
|
||||
|
||||
if val_proc:
|
||||
data = fill_vals_proc(data, uid, val_proc)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# get_uid
|
||||
# ---------------------------
|
||||
def get_uid(entry, key, key_secondary, key_search, keymap) -> Optional(str):
|
||||
"""Get UID for data list."""
|
||||
uid = None
|
||||
if not key_search:
|
||||
key_primary_found = key in entry
|
||||
if key_primary_found and key not in entry and not entry[key]:
|
||||
return None
|
||||
|
||||
if key_primary_found:
|
||||
uid = entry[key]
|
||||
elif key_secondary:
|
||||
if key_secondary not in entry:
|
||||
return None
|
||||
|
||||
if not entry[key_secondary]:
|
||||
return None
|
||||
|
||||
uid = entry[key_secondary]
|
||||
elif keymap and key_search in entry and entry[key_search] in keymap:
|
||||
uid = keymap[entry[key_search]]
|
||||
else:
|
||||
return None
|
||||
|
||||
return uid or None
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# generate_keymap
|
||||
# ---------------------------
|
||||
def generate_keymap(data, key_search) -> Optional(dict):
|
||||
"""Generate keymap."""
|
||||
return (
|
||||
{data[uid][key_search]: uid for uid in data if key_search in data[uid]}
|
||||
if key_search
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# matches_only
|
||||
# ---------------------------
|
||||
def matches_only(entry, only) -> bool:
|
||||
"""Return True if all variables are matched."""
|
||||
ret = False
|
||||
for val in only:
|
||||
if val["key"] in entry and entry[val["key"]] == val["value"]:
|
||||
ret = True
|
||||
else:
|
||||
ret = False
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# can_skip
|
||||
# ---------------------------
|
||||
def can_skip(entry, skip) -> bool:
|
||||
"""Return True if at least one variable matches."""
|
||||
ret = False
|
||||
for val in skip:
|
||||
if val["name"] in entry and entry[val["name"]] == val["value"]:
|
||||
ret = True
|
||||
break
|
||||
|
||||
if val["value"] == "" and val["name"] not in entry:
|
||||
ret = True
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# fill_defaults
|
||||
# ---------------------------
|
||||
def fill_defaults(data, vals) -> dict:
|
||||
"""Fill defaults if source is not present."""
|
||||
for val in vals:
|
||||
_name = val["name"]
|
||||
_type = val["type"] if "type" in val else "str"
|
||||
_source = val["source"] if "source" in val else _name
|
||||
|
||||
if _type == "str":
|
||||
_default = val["default"] if "default" in val else ""
|
||||
if "default_val" in val and val["default_val"] in val:
|
||||
_default = val[val["default_val"]]
|
||||
|
||||
if _name not in data:
|
||||
data[_name] = from_entry([], _source, default=_default)
|
||||
|
||||
elif _type == "bool":
|
||||
_default = val["default"] if "default" in val else False
|
||||
_reverse = val["reverse"] if "reverse" in val else False
|
||||
if _name not in data:
|
||||
data[_name] = from_entry_bool(
|
||||
[], _source, default=_default, reverse=_reverse
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# fill_vals
|
||||
# ---------------------------
|
||||
def fill_vals(data, entry, uid, vals) -> dict:
|
||||
"""Fill all data."""
|
||||
for val in vals:
|
||||
_name = val["name"]
|
||||
_type = val["type"] if "type" in val else "str"
|
||||
_source = val["source"] if "source" in val else _name
|
||||
_convert = val["convert"] if "convert" in val else None
|
||||
|
||||
if _type == "str":
|
||||
_default = val["default"] if "default" in val else ""
|
||||
if "default_val" in val and val["default_val"] in val:
|
||||
_default = val[val["default_val"]]
|
||||
|
||||
if uid:
|
||||
data[uid][_name] = from_entry(entry, _source, default=_default)
|
||||
else:
|
||||
data[_name] = from_entry(entry, _source, default=_default)
|
||||
|
||||
elif _type == "bool":
|
||||
_default = val["default"] if "default" in val else False
|
||||
_reverse = val["reverse"] if "reverse" in val else False
|
||||
|
||||
if uid:
|
||||
data[uid][_name] = from_entry_bool(
|
||||
entry, _source, default=_default, reverse=_reverse
|
||||
)
|
||||
else:
|
||||
data[_name] = from_entry_bool(
|
||||
entry, _source, default=_default, reverse=_reverse
|
||||
)
|
||||
|
||||
if _convert == "utc_from_timestamp":
|
||||
if uid:
|
||||
if isinstance(data[uid][_name], int) and data[uid][_name] > 0:
|
||||
if data[uid][_name] > 100000000000:
|
||||
data[uid][_name] = data[uid][_name] / 1000
|
||||
|
||||
data[uid][_name] = utc_from_timestamp(data[uid][_name])
|
||||
elif isinstance(data[_name], int) and data[_name] > 0:
|
||||
if data[_name] > 100000000000:
|
||||
data[_name] = data[_name] / 1000
|
||||
|
||||
data[_name] = utc_from_timestamp(data[_name])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# fill_ensure_vals
|
||||
# ---------------------------
|
||||
def fill_ensure_vals(data, uid, ensure_vals) -> dict:
|
||||
"""Add required keys which are not available in data."""
|
||||
for val in ensure_vals:
|
||||
if uid:
|
||||
if val["name"] not in data[uid]:
|
||||
_default = val["default"] if "default" in val else ""
|
||||
data[uid][val["name"]] = _default
|
||||
|
||||
elif val["name"] not in data:
|
||||
_default = val["default"] if "default" in val else ""
|
||||
data[val["name"]] = _default
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# fill_vals_proc
|
||||
# ---------------------------
|
||||
def fill_vals_proc(data, uid, vals_proc) -> dict:
|
||||
"""Add custom keys."""
|
||||
_data = data[uid] if uid else data
|
||||
for val_sub in vals_proc:
|
||||
_name = None
|
||||
_action = None
|
||||
_value = None
|
||||
for val in val_sub:
|
||||
if "name" in val:
|
||||
_name = val["name"]
|
||||
continue
|
||||
|
||||
if "action" in val:
|
||||
_action = val["action"]
|
||||
continue
|
||||
|
||||
if not _name and not _action:
|
||||
break
|
||||
|
||||
if _action == "combine":
|
||||
if "key" in val:
|
||||
tmp = _data[val["key"]] if val["key"] in _data else "unknown"
|
||||
_value = f"{_value}{tmp}" if _value else tmp
|
||||
|
||||
if "text" in val:
|
||||
tmp = val["text"]
|
||||
_value = f"{_value}{tmp}" if _value else tmp
|
||||
|
||||
if _name and _value:
|
||||
if uid:
|
||||
data[uid][_name] = _value
|
||||
else:
|
||||
data[_name] = _value
|
||||
|
||||
return data
|
||||
@@ -0,0 +1,352 @@
|
||||
"""TrueNAS binary sensor platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
from logging import getLogger
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .binary_sensor_types import (
|
||||
SENSOR_SERVICES,
|
||||
SENSOR_TYPES,
|
||||
)
|
||||
from .entity import TrueNASEntity, async_add_entities
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_setup_entry
|
||||
# ---------------------------
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
_async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up device tracker for OpenMediaVault component."""
|
||||
dispatcher = {
|
||||
"TrueNASBinarySensor": TrueNASBinarySensor,
|
||||
"TrueNASJailBinarySensor": TrueNASJailBinarySensor,
|
||||
"TrueNASVMBinarySensor": TrueNASVMBinarySensor,
|
||||
"TrueNASServiceBinarySensor": TrueNASServiceBinarySensor,
|
||||
"TrueNASAppBinarySensor": TrueNASAppBinarySensor,
|
||||
}
|
||||
await async_add_entities(hass, config_entry, dispatcher)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASBinarySensor
|
||||
# ---------------------------
|
||||
class TrueNASBinarySensor(TrueNASEntity, BinarySensorEntity):
|
||||
"""Define an TrueNAS Binary Sensor."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self._data[self.entity_description.data_is_on]
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
if self.entity_description.icon_enabled:
|
||||
if self._data[self.entity_description.data_is_on]:
|
||||
return self.entity_description.icon_enabled
|
||||
else:
|
||||
return self.entity_description.icon_disabled
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASJailBinarySensor
|
||||
# ---------------------------
|
||||
class TrueNASJailBinarySensor(TrueNASBinarySensor):
|
||||
"""Define a TrueNAS Jail Binary Sensor."""
|
||||
|
||||
async def start(self):
|
||||
"""Start a Jail."""
|
||||
tmp_jail = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"jail/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_jail:
|
||||
_LOGGER.error(
|
||||
"Jail %s (%s) invalid", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_jail["state"] != "down":
|
||||
_LOGGER.warning(
|
||||
"Jail %s (%s) is not down", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, "jail/start", "post", self._data["id"]
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop a Jail."""
|
||||
tmp_jail = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"jail/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_jail:
|
||||
_LOGGER.error(
|
||||
"Jail %s (%s) invalid", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_jail["state"] != "up":
|
||||
_LOGGER.warning(
|
||||
"Jail %s (%s) is not up", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, "jail/stop", "post", {"jail": self._data["id"]}
|
||||
)
|
||||
|
||||
async def restart(self):
|
||||
"""Restart a Jail."""
|
||||
tmp_jail = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"jail/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_jail:
|
||||
_LOGGER.error(
|
||||
"Jail %s (%s) invalid", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_jail["state"] != "up":
|
||||
_LOGGER.warning(
|
||||
"Jail %s (%s) is not up", self._data["comment"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, "jail/restart", "post", self._data["id"]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASVMBinarySensor
|
||||
# ---------------------------
|
||||
class TrueNASVMBinarySensor(TrueNASBinarySensor):
|
||||
"""Define a TrueNAS VM Binary Sensor."""
|
||||
|
||||
async def start(self, overcommit: bool = False):
|
||||
"""Start a VM."""
|
||||
tmp_vm = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"vm/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "status" not in tmp_vm:
|
||||
_LOGGER.error("VM %s (%s) invalid", self._data["name"], self._data["id"])
|
||||
return
|
||||
|
||||
if tmp_vm["status"]["state"] != "STOPPED":
|
||||
_LOGGER.warning(
|
||||
"VM %s (%s) is not down", self._data["name"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
f"vm/id/{self._data['id']}/start",
|
||||
"post",
|
||||
{"overcommit": overcommit},
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop a VM."""
|
||||
tmp_vm = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"vm/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "status" not in tmp_vm:
|
||||
_LOGGER.error("VM %s (%s) invalid", self._data["name"], self._data["id"])
|
||||
return
|
||||
|
||||
if tmp_vm["status"]["state"] != "RUNNING":
|
||||
_LOGGER.warning(
|
||||
"VM %s (%s) is not up", self._data["name"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"vm/id/{self._data['id']}/stop", "post"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASServiceBinarySensor
|
||||
# ---------------------------
|
||||
class TrueNASServiceBinarySensor(TrueNASBinarySensor):
|
||||
"""Define a TrueNAS Service Binary Sensor."""
|
||||
|
||||
async def start(self):
|
||||
"""Start a Service."""
|
||||
tmp_service = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"service/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_service:
|
||||
_LOGGER.error(
|
||||
"Service %s (%s) invalid", self._data["service"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_service["state"] != "STOPPED":
|
||||
_LOGGER.warning(
|
||||
"Service %s (%s) is not stopped",
|
||||
self._data["service"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"service/start",
|
||||
"post",
|
||||
{"service": self._data["service"]},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def stop(self):
|
||||
"""Stop a Service."""
|
||||
tmp_service = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"service/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_service:
|
||||
_LOGGER.error(
|
||||
"Service %s (%s) invalid", self._data["service"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_service["state"] == "STOPPED":
|
||||
_LOGGER.warning(
|
||||
"Service %s (%s) is not running",
|
||||
self._data["service"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"service/stop",
|
||||
"post",
|
||||
{"service": self._data["service"]},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def restart(self):
|
||||
"""Restart a Service."""
|
||||
tmp_service = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"service/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_service:
|
||||
_LOGGER.error(
|
||||
"Service %s (%s) invalid", self._data["service"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_service["state"] == "STOPPED":
|
||||
_LOGGER.warning(
|
||||
"Service %s (%s) is not running",
|
||||
self._data["service"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"service/restart",
|
||||
"post",
|
||||
{"service": self._data["service"]},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def reload(self):
|
||||
"""Reload a Service."""
|
||||
tmp_service = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"service/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "state" not in tmp_service:
|
||||
_LOGGER.error(
|
||||
"Service %s (%s) invalid", self._data["service"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
if tmp_service["state"] == "STOPPED":
|
||||
_LOGGER.warning(
|
||||
"Service %s (%s) is not running",
|
||||
self._data["service"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"service/reload",
|
||||
"post",
|
||||
{"service": self._data["service"]},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASAppsBinarySensor
|
||||
# ---------------------------
|
||||
class TrueNASAppBinarySensor(TrueNASBinarySensor):
|
||||
"""Define a TrueNAS Applications Binary Sensor."""
|
||||
|
||||
async def start(self):
|
||||
"""Start a VM."""
|
||||
tmp_vm = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"/chart/release/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "status" not in tmp_vm:
|
||||
_LOGGER.error("VM %s (%s) invalid", self._data["name"], self._data["id"])
|
||||
return
|
||||
|
||||
if tmp_vm["status"] == "ACTIVE":
|
||||
_LOGGER.warning(
|
||||
"VM %s (%s) is not down", self._data["name"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"/chart/release/scale",
|
||||
"post",
|
||||
{"release_name": self._data["id"], "scale_options": {"replica_count": 1}},
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""Stop a VM."""
|
||||
tmp_vm = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"/chart/release/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "status" not in tmp_vm:
|
||||
_LOGGER.error("VM %s (%s) invalid", self._data["name"], self._data["id"])
|
||||
return
|
||||
|
||||
if tmp_vm["status"] != "ACTIVE":
|
||||
_LOGGER.warning(
|
||||
"VM %s (%s) is not up", self._data["name"], self._data["id"]
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"/chart/release/scale",
|
||||
"post",
|
||||
{"release_name": self._data["id"], "scale_options": {"replica_count": 0}},
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Definitions for TrueNAS binary sensor entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
SCHEMA_SERVICE_APP_START,
|
||||
SCHEMA_SERVICE_APP_STOP,
|
||||
SCHEMA_SERVICE_JAIL_RESTART,
|
||||
SCHEMA_SERVICE_JAIL_START,
|
||||
SCHEMA_SERVICE_JAIL_STOP,
|
||||
SCHEMA_SERVICE_SERVICE_RELOAD,
|
||||
SCHEMA_SERVICE_SERVICE_RESTART,
|
||||
SCHEMA_SERVICE_SERVICE_START,
|
||||
SCHEMA_SERVICE_SERVICE_STOP,
|
||||
SCHEMA_SERVICE_VM_START,
|
||||
SCHEMA_SERVICE_VM_STOP,
|
||||
SERVICE_APP_START,
|
||||
SERVICE_APP_STOP,
|
||||
SERVICE_JAIL_RESTART,
|
||||
SERVICE_JAIL_START,
|
||||
SERVICE_JAIL_STOP,
|
||||
SERVICE_SERVICE_RELOAD,
|
||||
SERVICE_SERVICE_RESTART,
|
||||
SERVICE_SERVICE_START,
|
||||
SERVICE_SERVICE_STOP,
|
||||
SERVICE_VM_START,
|
||||
SERVICE_VM_STOP,
|
||||
)
|
||||
|
||||
DEVICE_ATTRIBUTES_POOL = [
|
||||
"path",
|
||||
"status",
|
||||
"healthy",
|
||||
"is_decrypted",
|
||||
"autotrim",
|
||||
"scrub_state",
|
||||
"scrub_start",
|
||||
"scrub_end",
|
||||
"scrub_secs_left",
|
||||
"available_gib",
|
||||
"total_gib",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_JAIL = [
|
||||
"comment",
|
||||
"jail_zfs_dataset",
|
||||
"last_started",
|
||||
"ip4_addr",
|
||||
"ip6_addr",
|
||||
"release",
|
||||
"type",
|
||||
"plugin_name",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_VM = [
|
||||
"description",
|
||||
"vcpus",
|
||||
"memory",
|
||||
"autostart",
|
||||
"cores",
|
||||
"threads",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_SERVICE = [
|
||||
"enable",
|
||||
"state",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_APP = [
|
||||
"name",
|
||||
"version",
|
||||
"human_version",
|
||||
"update_available",
|
||||
"image_updates_available",
|
||||
"portal",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrueNASBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing entities."""
|
||||
|
||||
icon_enabled: str | None = None
|
||||
icon_disabled: str | None = None
|
||||
ha_group: str | None = None
|
||||
ha_connection: str | None = None
|
||||
ha_connection_value: str | None = None
|
||||
data_path: str | None = None
|
||||
data_is_on: str = "available"
|
||||
data_name: str | None = None
|
||||
data_uid: str | None = None
|
||||
data_reference: str | None = None
|
||||
data_attributes_list: List = field(default_factory=lambda: [])
|
||||
func: str = "TrueNASBinarySensor"
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
TrueNASBinarySensorEntityDescription(
|
||||
key="pool_healthy",
|
||||
name="healthy",
|
||||
icon_enabled="mdi:database",
|
||||
icon_disabled="mdi:database-off",
|
||||
device_class=None,
|
||||
entity_category=None,
|
||||
ha_group="System",
|
||||
data_path="pool",
|
||||
data_is_on="healthy",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="guid",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_POOL,
|
||||
),
|
||||
TrueNASBinarySensorEntityDescription(
|
||||
key="jail",
|
||||
name="",
|
||||
icon_enabled="mdi:layers",
|
||||
icon_disabled="mdi:layers-off",
|
||||
device_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Jails",
|
||||
data_path="jail",
|
||||
data_is_on="state",
|
||||
data_name="host_hostname",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_JAIL,
|
||||
func="TrueNASJailBinarySensor",
|
||||
),
|
||||
TrueNASBinarySensorEntityDescription(
|
||||
key="vm",
|
||||
name="",
|
||||
icon_enabled="mdi:server",
|
||||
icon_disabled="mdi:server-off",
|
||||
device_class=None,
|
||||
entity_category=None,
|
||||
ha_group="VMs",
|
||||
data_path="vm",
|
||||
data_is_on="running",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_VM,
|
||||
func="TrueNASVMBinarySensor",
|
||||
),
|
||||
TrueNASBinarySensorEntityDescription(
|
||||
key="service",
|
||||
name="",
|
||||
icon_enabled="mdi:cog",
|
||||
icon_disabled="mdi:cog-off",
|
||||
device_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Services",
|
||||
data_path="service",
|
||||
data_is_on="running",
|
||||
data_name="service",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_SERVICE,
|
||||
func="TrueNASServiceBinarySensor",
|
||||
),
|
||||
TrueNASBinarySensorEntityDescription(
|
||||
key="app",
|
||||
name="",
|
||||
icon_enabled="mdi:server",
|
||||
icon_disabled="mdi:server-off",
|
||||
device_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Apps",
|
||||
data_path="app",
|
||||
data_is_on="running",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_APP,
|
||||
func="TrueNASAppBinarySensor",
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_SERVICES = [
|
||||
[SERVICE_JAIL_START, SCHEMA_SERVICE_JAIL_START, "start"],
|
||||
[SERVICE_JAIL_STOP, SCHEMA_SERVICE_JAIL_STOP, "stop"],
|
||||
[SERVICE_JAIL_RESTART, SCHEMA_SERVICE_JAIL_RESTART, "restart"],
|
||||
[SERVICE_VM_START, SCHEMA_SERVICE_VM_START, "start"],
|
||||
[SERVICE_VM_STOP, SCHEMA_SERVICE_VM_STOP, "stop"],
|
||||
[SERVICE_SERVICE_START, SCHEMA_SERVICE_SERVICE_START, "start"],
|
||||
[SERVICE_SERVICE_STOP, SCHEMA_SERVICE_SERVICE_STOP, "stop"],
|
||||
[SERVICE_SERVICE_RESTART, SCHEMA_SERVICE_SERVICE_RESTART, "restart"],
|
||||
[SERVICE_SERVICE_RELOAD, SCHEMA_SERVICE_SERVICE_RELOAD, "reload"],
|
||||
[SERVICE_APP_START, SCHEMA_SERVICE_APP_START, "start"],
|
||||
[SERVICE_APP_STOP, SCHEMA_SERVICE_APP_STOP, "stop"],
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Config flow to configure TrueNAS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import (
|
||||
DEFAULT_DEVICE_NAME,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_SSL,
|
||||
DEFAULT_SSL_VERIFY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .api import TrueNASAPI
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# configured_instances
|
||||
# ---------------------------
|
||||
@callback
|
||||
def configured_instances(hass):
|
||||
"""Return a set of configured instances."""
|
||||
return {
|
||||
entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASConfigFlow
|
||||
# ---------------------------
|
||||
class TrueNASConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""TrueNASConfigFlow class."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||
|
||||
async def async_step_import(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Occurs when a previous entry setup fails and is re-initiated."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
# Check if instance with this name already exists
|
||||
if user_input[CONF_NAME] in configured_instances(self.hass):
|
||||
errors["base"] = "name_exists"
|
||||
|
||||
# Test connection
|
||||
api = await self.hass.async_add_executor_job(
|
||||
TrueNASAPI,
|
||||
self.hass,
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_API_KEY],
|
||||
user_input[CONF_SSL],
|
||||
user_input[CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
conn, errorcode = await self.hass.async_add_executor_job(
|
||||
api.connection_test
|
||||
)
|
||||
if not conn:
|
||||
errors[CONF_HOST] = errorcode
|
||||
_LOGGER.error("TrueNAS connection error (%s)", errorcode)
|
||||
|
||||
# Save instance
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
return self._show_config_form(user_input=user_input, errors=errors)
|
||||
|
||||
return self._show_config_form(
|
||||
user_input={
|
||||
CONF_NAME: DEFAULT_DEVICE_NAME,
|
||||
CONF_HOST: DEFAULT_HOST,
|
||||
CONF_API_KEY: "",
|
||||
CONF_SSL: DEFAULT_SSL,
|
||||
CONF_VERIFY_SSL: DEFAULT_SSL_VERIFY,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
def _show_config_form(
|
||||
self, user_input: dict[str, Any] | None, errors: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Show the configuration form."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str,
|
||||
vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
|
||||
vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str,
|
||||
vol.Optional(CONF_SSL, default=user_input[CONF_SSL]): bool,
|
||||
vol.Optional(
|
||||
CONF_VERIFY_SSL, default=user_input[CONF_VERIFY_SSL]
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Constants used by the TrueNAS integration."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
DOMAIN = "truenas"
|
||||
DEFAULT_NAME = "root"
|
||||
ATTRIBUTION = "Data provided by TrueNAS integration"
|
||||
|
||||
DEFAULT_HOST = "10.0.0.1"
|
||||
DEFAULT_USERNAME = "admin"
|
||||
|
||||
DEFAULT_DEVICE_NAME = "TrueNAS"
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_SSL_VERIFY = True
|
||||
|
||||
TO_REDACT = {
|
||||
"username",
|
||||
"password",
|
||||
"encryption_password",
|
||||
"encryption_salt",
|
||||
"host",
|
||||
"api_key",
|
||||
"serial",
|
||||
"system_serial",
|
||||
"ip4_addr",
|
||||
"ip6_addr",
|
||||
"account",
|
||||
"key",
|
||||
}
|
||||
|
||||
SERVICE_CLOUDSYNC_RUN = "cloudsync_run"
|
||||
SCHEMA_SERVICE_CLOUDSYNC_RUN = {}
|
||||
|
||||
SERVICE_CLOUDSYNC_ABORT = "cloudsync_abort"
|
||||
SCHEMA_SERVICE_CLOUDSYNC_ABORT = {}
|
||||
|
||||
SERVICE_DATASET_SNAPSHOT = "dataset_snapshot"
|
||||
SCHEMA_SERVICE_DATASET_SNAPSHOT = {}
|
||||
|
||||
SERVICE_SYSTEM_REBOOT = "system_reboot"
|
||||
SCHEMA_SERVICE_SYSTEM_REBOOT = {}
|
||||
|
||||
SERVICE_SYSTEM_SHUTDOWN = "system_shutdown"
|
||||
SCHEMA_SERVICE_SYSTEM_SHUTDOWN = {}
|
||||
|
||||
SERVICE_SERVICE_START = "service_start"
|
||||
SCHEMA_SERVICE_SERVICE_START = {}
|
||||
SERVICE_SERVICE_STOP = "service_stop"
|
||||
SCHEMA_SERVICE_SERVICE_STOP = {}
|
||||
SERVICE_SERVICE_RESTART = "service_restart"
|
||||
SCHEMA_SERVICE_SERVICE_RESTART = {}
|
||||
SERVICE_SERVICE_RELOAD = "service_reload"
|
||||
SCHEMA_SERVICE_SERVICE_RELOAD = {}
|
||||
|
||||
SERVICE_JAIL_START = "jail_start"
|
||||
SCHEMA_SERVICE_JAIL_START = {}
|
||||
SERVICE_JAIL_STOP = "jail_stop"
|
||||
SCHEMA_SERVICE_JAIL_STOP = {}
|
||||
SERVICE_JAIL_RESTART = "jail_restart"
|
||||
SCHEMA_SERVICE_JAIL_RESTART = {}
|
||||
|
||||
SERVICE_VM_START = "vm_start"
|
||||
SERVICE_VM_START_OVERCOMMIT = "overcommit"
|
||||
SCHEMA_SERVICE_VM_START = {vol.Optional(SERVICE_VM_START_OVERCOMMIT): cv.boolean}
|
||||
SERVICE_VM_STOP = "vm_stop"
|
||||
SCHEMA_SERVICE_VM_STOP = {}
|
||||
|
||||
SERVICE_APP_START = "app_start"
|
||||
SCHEMA_SERVICE_APP_START = {}
|
||||
SERVICE_APP_STOP = "app_stop"
|
||||
SCHEMA_SERVICE_APP_STOP = {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
"""Diagnostics support for TrueNAS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, TO_REDACT
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return {
|
||||
"entry": {
|
||||
"data": async_redact_data(config_entry.data, TO_REDACT),
|
||||
"options": async_redact_data(config_entry.options, TO_REDACT),
|
||||
},
|
||||
"data": async_redact_data(
|
||||
hass.data[DOMAIN][config_entry.entry_id].ds, TO_REDACT
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
"""TrueNAS HA shared entity model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from logging import getLogger
|
||||
from typing import Any, Callable
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
entity_platform as ep,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import TrueNASCoordinator
|
||||
from .helper import format_attribute
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_add_entities
|
||||
# ---------------------------
|
||||
async def async_add_entities(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, dispatcher: dict[str, Callable]
|
||||
):
|
||||
"""Add entities."""
|
||||
platform = ep.async_get_current_platform()
|
||||
services = platform.platform.SENSOR_SERVICES
|
||||
descriptions = platform.platform.SENSOR_TYPES
|
||||
|
||||
for service in services:
|
||||
platform.async_register_entity_service(service[0], service[1], service[2])
|
||||
|
||||
@callback
|
||||
async def async_update_controller(coordinator):
|
||||
"""Update the values of the controller."""
|
||||
|
||||
async def async_check_exist(obj, coordinator, uid: None) -> None:
|
||||
"""Check entity exists."""
|
||||
entity_registry = er.async_get(hass)
|
||||
if uid:
|
||||
unique_id = f"{obj._inst.lower()}-{obj.entity_description.key}-{slugify(str(obj._data[obj.entity_description.data_reference]).lower())}"
|
||||
else:
|
||||
unique_id = f"{obj._inst.lower()}-{obj.entity_description.key}"
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
platform.domain, DOMAIN, unique_id
|
||||
)
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
if entity is None or (
|
||||
(entity_id not in platform.entities) and (entity.disabled is False)
|
||||
):
|
||||
_LOGGER.debug("Add entity %s", entity_id)
|
||||
await platform.async_add_entities([obj])
|
||||
|
||||
for entity_description in descriptions:
|
||||
data = coordinator.data[entity_description.data_path]
|
||||
if not entity_description.data_reference:
|
||||
if data.get(entity_description.data_attribute) is None:
|
||||
continue
|
||||
obj = dispatcher[entity_description.func](
|
||||
coordinator, entity_description
|
||||
)
|
||||
await async_check_exist(obj, coordinator, None)
|
||||
else:
|
||||
for uid in data:
|
||||
obj = dispatcher[entity_description.func](
|
||||
coordinator, entity_description, uid
|
||||
)
|
||||
await async_check_exist(obj, coordinator, uid)
|
||||
|
||||
await async_update_controller(hass.data[DOMAIN][config_entry.entry_id])
|
||||
|
||||
unsub = async_dispatcher_connect(hass, "update_sensors", async_update_controller)
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASEntity
|
||||
# ---------------------------
|
||||
class TrueNASEntity(CoordinatorEntity[TrueNASCoordinator], Entity):
|
||||
"""Define entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TrueNASCoordinator,
|
||||
entity_description,
|
||||
uid: str | None = None,
|
||||
):
|
||||
"""Initialize entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._inst = coordinator.config_entry.data[CONF_NAME]
|
||||
self._config_entry = self.coordinator.config_entry
|
||||
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
self._uid = uid
|
||||
if self._uid:
|
||||
self._data = coordinator.data[self.entity_description.data_path][self._uid]
|
||||
else:
|
||||
self._data = coordinator.data[self.entity_description.data_path]
|
||||
|
||||
platform = ep.async_get_current_platform()
|
||||
|
||||
dev_group = self.entity_description.ha_group
|
||||
if self.entity_description.ha_group.startswith("data__"):
|
||||
dev_group = self.entity_description.ha_group[6:]
|
||||
if dev_group in self._data:
|
||||
dev_group = self._data[dev_group]
|
||||
|
||||
self.entity_id = f"{platform.domain}.{self._inst.lower()}_{slugify(str(dev_group).lower())}_{slugify(str(self.name).lower())}"
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
self._data = self.coordinator.data[self.entity_description.data_path]
|
||||
if self._uid:
|
||||
self._data = self.coordinator.data[self.entity_description.data_path][
|
||||
self._uid
|
||||
]
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name for this entity."""
|
||||
if not self._uid:
|
||||
return f"{self.entity_description.name}"
|
||||
|
||||
if self.entity_description.name:
|
||||
return f"{self._data[self.entity_description.data_name]} {self.entity_description.name}"
|
||||
|
||||
return f"{self._data[self.entity_description.data_name]}"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique id for this entity."""
|
||||
if self._uid:
|
||||
return f"{self._inst.lower()}-{self.entity_description.key}-{slugify(str(self._data[self.entity_description.data_reference]).lower())}"
|
||||
else:
|
||||
return f"{self._inst.lower()}-{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return a description for device registry."""
|
||||
dev_connection = DOMAIN
|
||||
dev_connection_value = f"{self._inst}_{self.entity_description.ha_group}"
|
||||
dev_group = self.entity_description.ha_group
|
||||
if self.entity_description.ha_group == "System":
|
||||
dev_connection_value = (
|
||||
f"{self._inst}_{self.coordinator.data['system_info']['hostname']}"
|
||||
)
|
||||
|
||||
if self.entity_description.ha_group.startswith("data__"):
|
||||
dev_group = self.entity_description.ha_group[6:]
|
||||
if dev_group in self._data:
|
||||
dev_group = self._data[dev_group]
|
||||
dev_connection_value = dev_group
|
||||
|
||||
if self.entity_description.ha_connection:
|
||||
dev_connection = self.entity_description.ha_connection
|
||||
|
||||
if self.entity_description.ha_connection_value:
|
||||
dev_connection_value = self.entity_description.ha_connection_value
|
||||
if dev_connection_value.startswith("data__"):
|
||||
dev_connection_value = f"{self._inst}_{dev_connection_value[6:]}"
|
||||
dev_connection_value = (
|
||||
f"{self._inst}_{self._data[dev_connection_value]}"
|
||||
)
|
||||
|
||||
if self.entity_description.ha_group == "System":
|
||||
return DeviceInfo(
|
||||
connections={(dev_connection, f"{dev_connection_value}")},
|
||||
identifiers={(dev_connection, f"{dev_connection_value}")},
|
||||
name=dev_group,
|
||||
model=f"{self.coordinator.data['system_info']['system_product']}",
|
||||
manufacturer=f"{self.coordinator.data['system_info']['system_manufacturer']}",
|
||||
sw_version=f"{self.coordinator.data['system_info']['version']}",
|
||||
configuration_url=f"http://{self.coordinator.config_entry.data[CONF_HOST]}",
|
||||
)
|
||||
else:
|
||||
return DeviceInfo(
|
||||
connections={(dev_connection, f"{dev_connection_value}")},
|
||||
default_name=dev_group,
|
||||
default_model=f"{self.coordinator.data['system_info']['system_product']}",
|
||||
default_manufacturer=f"{self.coordinator.data['system_info']['system_manufacturer']}",
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
f"{self._inst}_{self.coordinator.data['system_info']['hostname']}",
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
attributes = super().extra_state_attributes
|
||||
for variable in self.entity_description.data_attributes_list:
|
||||
if variable in self._data:
|
||||
attributes[format_attribute(variable)] = self._data[variable]
|
||||
|
||||
return attributes
|
||||
|
||||
async def start(self):
|
||||
"""Run function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def stop(self):
|
||||
"""Stop function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def restart(self):
|
||||
"""Restart function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def reload(self):
|
||||
"""Reload function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def snapshot(self):
|
||||
"""Snapshot function."""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Helper functions."""
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# format_attribute
|
||||
# ---------------------------
|
||||
def format_attribute(attr: str) -> str:
|
||||
"""Format attribute."""
|
||||
res = attr.replace("_", " ")
|
||||
res = res.replace("-", " ")
|
||||
res = res.capitalize()
|
||||
res = res.replace("zfs", "ZFS")
|
||||
res = res.replace(" gib", " GiB")
|
||||
res = res.replace("Cpu ", "CPU ")
|
||||
res = res.replace("Vcpu ", "vCPU ")
|
||||
res = res.replace("Vmware ", "VMware ")
|
||||
res = res.replace("Ip4 ", "IP4 ")
|
||||
res = res.replace("Ip6 ", "IP6 ")
|
||||
return res
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"domain": "truenas",
|
||||
"name": "TrueNAS",
|
||||
"codeowners": [
|
||||
"@tomaae"
|
||||
],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://github.com/tomaae/homeassistant-truenas",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/tomaae/homeassistant-truenas/issues",
|
||||
"requirements": [],
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
"""TrueNAS sensor platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .coordinator import TrueNASCoordinator
|
||||
from .entity import TrueNASEntity, async_add_entities
|
||||
from .sensor_types import (
|
||||
SENSOR_SERVICES,
|
||||
SENSOR_TYPES,
|
||||
)
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_setup_entry
|
||||
# ---------------------------
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
_async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up entry for TrueNAS component."""
|
||||
dispatcher = {
|
||||
"TrueNASSensor": TrueNASSensor,
|
||||
"TrueNASUptimeSensor": TrueNASUptimeSensor,
|
||||
"TrueNASClousyncSensor": TrueNASClousyncSensor,
|
||||
"TrueNASDatasetSensor": TrueNASDatasetSensor,
|
||||
}
|
||||
await async_add_entities(hass, config_entry, dispatcher)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASSensor
|
||||
# ---------------------------
|
||||
class TrueNASSensor(TrueNASEntity, SensorEntity):
|
||||
"""Define an TrueNAS sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TrueNASCoordinator,
|
||||
entity_description,
|
||||
uid: str | None = None,
|
||||
):
|
||||
super().__init__(coordinator, entity_description, uid)
|
||||
self._attr_suggested_unit_of_measurement = (
|
||||
self.entity_description.suggested_unit_of_measurement
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | date | datetime | Decimal:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self._data[self.entity_description.data_attribute]
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
if self.entity_description.native_unit_of_measurement:
|
||||
if self.entity_description.native_unit_of_measurement.startswith("data__"):
|
||||
uom = self.entity_description.native_unit_of_measurement[6:]
|
||||
if uom in self._data:
|
||||
return self._data[uom]
|
||||
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASUptimeSensor
|
||||
# ---------------------------
|
||||
class TrueNASUptimeSensor(TrueNASSensor):
|
||||
"""Define an TrueNAS Uptime sensor."""
|
||||
|
||||
async def restart(self) -> None:
|
||||
"""Restart TrueNAS systen."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"system/reboot",
|
||||
"post",
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Shutdown TrueNAS systen."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"system/shutdown",
|
||||
"post",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASDatasetSensor
|
||||
# ---------------------------
|
||||
class TrueNASDatasetSensor(TrueNASSensor):
|
||||
"""Define an TrueNAS Dataset sensor."""
|
||||
|
||||
async def snapshot(self) -> None:
|
||||
"""Create dataset snapshot."""
|
||||
ts = datetime.now().isoformat(sep="_", timespec="microseconds")
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"zfs/snapshot",
|
||||
"post",
|
||||
{"dataset": f"{self._data['name']}", "name": f"custom-{ts}"},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASClousyncSensor
|
||||
# ---------------------------
|
||||
class TrueNASClousyncSensor(TrueNASSensor):
|
||||
"""Define an TrueNAS Cloudsync sensor."""
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Run cloudsync job."""
|
||||
tmp_job = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"cloudsync/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "job" not in tmp_job:
|
||||
_LOGGER.error(
|
||||
"Clousync job %s (%s) invalid",
|
||||
self._data["description"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
if tmp_job["job"]["state"] in ["WAITING", "RUNNING"]:
|
||||
_LOGGER.warning(
|
||||
"Clousync job %s (%s) is already running",
|
||||
self._data["description"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"cloudsync/id/{self._data['id']}/sync", "post"
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Abort cloudsync job."""
|
||||
tmp_job = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query, f"cloudsync/id/{self._data['id']}"
|
||||
)
|
||||
|
||||
if "job" not in tmp_job:
|
||||
_LOGGER.error(
|
||||
"Clousync job %s (%s) invalid",
|
||||
self._data["description"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
if tmp_job["job"]["state"] not in ["WAITING", "RUNNING"]:
|
||||
_LOGGER.warning(
|
||||
"Clousync job %s (%s) is not running",
|
||||
self._data["description"],
|
||||
self._data["id"],
|
||||
)
|
||||
return
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
f"cloudsync/id/{self._data['id']}/abort",
|
||||
"post",
|
||||
None,
|
||||
)
|
||||
@@ -0,0 +1,445 @@
|
||||
"""Definitions for TrueNAS sensor entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UnitOfTemperature,
|
||||
UnitOfDataRate,
|
||||
UnitOfInformation,
|
||||
)
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
from .const import (
|
||||
SCHEMA_SERVICE_CLOUDSYNC_RUN,
|
||||
SCHEMA_SERVICE_CLOUDSYNC_ABORT,
|
||||
SCHEMA_SERVICE_DATASET_SNAPSHOT,
|
||||
SCHEMA_SERVICE_SYSTEM_REBOOT,
|
||||
SCHEMA_SERVICE_SYSTEM_SHUTDOWN,
|
||||
SERVICE_CLOUDSYNC_RUN,
|
||||
SERVICE_CLOUDSYNC_ABORT,
|
||||
SERVICE_DATASET_SNAPSHOT,
|
||||
SERVICE_SYSTEM_REBOOT,
|
||||
SERVICE_SYSTEM_SHUTDOWN,
|
||||
)
|
||||
|
||||
DEVICE_ATTRIBUTES_NETWORK = [
|
||||
"description",
|
||||
"mtu",
|
||||
"link_state",
|
||||
"active_media_type",
|
||||
"active_media_subtype",
|
||||
"link_address",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_POOL = [
|
||||
"path",
|
||||
"status",
|
||||
"healthy",
|
||||
"is_decrypted",
|
||||
"autotrim",
|
||||
"scrub_state",
|
||||
"scrub_start",
|
||||
"scrub_end",
|
||||
"scrub_secs_left",
|
||||
"healthy",
|
||||
"usage",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_DATASET = [
|
||||
"type",
|
||||
"pool",
|
||||
"mountpoint",
|
||||
"deduplication",
|
||||
"atime",
|
||||
"casesensitivity",
|
||||
"checksum",
|
||||
"exec",
|
||||
"sync",
|
||||
"compression",
|
||||
"compressratio",
|
||||
"quota",
|
||||
"copies",
|
||||
"readonly",
|
||||
"recordsize",
|
||||
"encryption_algorithm",
|
||||
"used",
|
||||
"available",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_DISK = [
|
||||
"serial",
|
||||
"size",
|
||||
"hddstandby",
|
||||
"hddstandby_force",
|
||||
"advpowermgmt",
|
||||
"acousticlevel",
|
||||
"togglesmart",
|
||||
"model",
|
||||
"rotationrate",
|
||||
"type",
|
||||
"name",
|
||||
"devname",
|
||||
"zfs_guid",
|
||||
"identifier",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_CPU = [
|
||||
"cpu_softirq",
|
||||
"cpu_system",
|
||||
"cpu_user",
|
||||
"cpu_nice",
|
||||
"cpu_iowait",
|
||||
"cpu_idle",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_MEMORY = [
|
||||
"memory-used_value",
|
||||
"memory-free_value",
|
||||
"memory-cached_value",
|
||||
"memory-buffered_value",
|
||||
"memory-total_value",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_CLOUDSYNC = [
|
||||
"direction",
|
||||
"path",
|
||||
"enabled",
|
||||
"transfer_mode",
|
||||
"snapshot",
|
||||
"time_started",
|
||||
"time_finished",
|
||||
"job_percent",
|
||||
"job_description",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_REPLICATION = [
|
||||
"source_datasets",
|
||||
"target_dataset",
|
||||
"recursive",
|
||||
"enabled",
|
||||
"direction",
|
||||
"transport",
|
||||
"auto",
|
||||
"retention_policy",
|
||||
"state",
|
||||
"time_started",
|
||||
"time_finished",
|
||||
"job_percent",
|
||||
"job_description",
|
||||
]
|
||||
|
||||
DEVICE_ATTRIBUTES_SNAPSHOTTASK = [
|
||||
"recursive",
|
||||
"lifetime_value",
|
||||
"lifetime_unit",
|
||||
"enabled",
|
||||
"naming_schema",
|
||||
"allow_empty",
|
||||
"vmware_sync",
|
||||
"state",
|
||||
"datetime",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrueNASSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing entities."""
|
||||
|
||||
ha_group: str | None = None
|
||||
ha_connection: str | None = None
|
||||
ha_connection_value: str | None = None
|
||||
data_path: str | None = None
|
||||
data_attribute: str | None = None
|
||||
data_name: str | None = None
|
||||
data_uid: str | None = None
|
||||
data_reference: str | None = None
|
||||
data_attributes_list: List = field(default_factory=lambda: [])
|
||||
func: str = "TrueNASSensor"
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[TrueNASSensorEntityDescription, ...] = (
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_uptime",
|
||||
name="Uptime",
|
||||
icon="mdi:clock-outline",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="uptimeEpoch",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
func="TrueNASUptimeSensor",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_cpu_temperature",
|
||||
name="Temperature",
|
||||
icon="mdi:thermometer",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="cpu_temperature",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_load_shortterm",
|
||||
name="CPU load shortterm",
|
||||
icon="mdi:gauge",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="load_shortterm",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_load_midterm",
|
||||
name="CPU load midterm",
|
||||
icon="mdi:gauge",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="load_midterm",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_load_longterm",
|
||||
name="CPU load longterm",
|
||||
icon="mdi:gauge",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="load_longterm",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_cpu_usage",
|
||||
name="CPU usage",
|
||||
icon="mdi:cpu-64-bit",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="cpu_usage",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_CPU,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_memory_usage",
|
||||
name="Memory usage",
|
||||
icon="mdi:memory",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="memory-usage_percent",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_MEMORY,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="system_cache_size-arc_value",
|
||||
name="ARC size",
|
||||
icon="mdi:memory",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_display_precision=1,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ha_group="System",
|
||||
data_path="system_info",
|
||||
data_attribute="cache_size-arc_value",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="dataset",
|
||||
name="",
|
||||
icon="mdi:database",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=None,
|
||||
ha_group="Datasets",
|
||||
data_path="dataset",
|
||||
data_attribute="used_gb",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_DATASET,
|
||||
func="TrueNASDatasetSensor",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="disk",
|
||||
name="",
|
||||
icon="mdi:harddisk",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=None,
|
||||
ha_group="Disks",
|
||||
data_path="disk",
|
||||
data_attribute="temperature",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="identifier",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_DISK,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="pool_free",
|
||||
name="free",
|
||||
icon="mdi:database-settings",
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=None,
|
||||
ha_group="System",
|
||||
data_path="pool",
|
||||
data_attribute="available_gib",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="guid",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_POOL,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="cloudsync",
|
||||
name="",
|
||||
icon="mdi:cloud-upload",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Cloudsync",
|
||||
data_path="cloudsync",
|
||||
data_attribute="state",
|
||||
data_name="description",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_CLOUDSYNC,
|
||||
func="TrueNASClousyncSensor",
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="replication",
|
||||
name="",
|
||||
icon="mdi:transfer",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Replication",
|
||||
data_path="replication",
|
||||
data_attribute="state",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_REPLICATION,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="snapshottask",
|
||||
name="",
|
||||
icon="mdi:checkbox-marked-circle-plus-outline",
|
||||
native_unit_of_measurement=None,
|
||||
device_class=None,
|
||||
state_class=None,
|
||||
entity_category=None,
|
||||
ha_group="Snapshot tasks",
|
||||
data_path="snapshottask",
|
||||
data_attribute="state",
|
||||
data_name="dataset",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_SNAPSHOTTASK,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="traffic_rx",
|
||||
name="RX",
|
||||
icon="mdi:download-network-outline",
|
||||
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=None,
|
||||
ha_group="System",
|
||||
data_path="interface",
|
||||
data_attribute="rx",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_NETWORK,
|
||||
),
|
||||
TrueNASSensorEntityDescription(
|
||||
key="traffic_tx",
|
||||
name="TX",
|
||||
icon="mdi:upload-network-outline",
|
||||
native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=None,
|
||||
ha_group="System",
|
||||
data_path="interface",
|
||||
data_attribute="tx",
|
||||
data_name="name",
|
||||
data_uid="",
|
||||
data_reference="id",
|
||||
data_attributes_list=DEVICE_ATTRIBUTES_NETWORK,
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_SERVICES = [
|
||||
[SERVICE_CLOUDSYNC_RUN, SCHEMA_SERVICE_CLOUDSYNC_RUN, "start"],
|
||||
[SERVICE_CLOUDSYNC_ABORT, SCHEMA_SERVICE_CLOUDSYNC_ABORT, "stop"],
|
||||
[SERVICE_DATASET_SNAPSHOT, SCHEMA_SERVICE_DATASET_SNAPSHOT, "snapshot"],
|
||||
[SERVICE_SYSTEM_REBOOT, SCHEMA_SERVICE_SYSTEM_REBOOT, "restart"],
|
||||
[SERVICE_SYSTEM_SHUTDOWN, SCHEMA_SERVICE_SYSTEM_SHUTDOWN, "stop"],
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
cloudsync_run:
|
||||
name: Cloudsync Run
|
||||
description: Start a Clousync Job
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: sensor
|
||||
|
||||
cloudsync_abort:
|
||||
name: Cloudsync Abort
|
||||
description: Abort a Clousync Job
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: sensor
|
||||
|
||||
dataset_snapshot:
|
||||
name: Dataset Snapshot
|
||||
description: Create a Dataset Snapshot
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: sensor
|
||||
|
||||
system_reboot:
|
||||
name: Reboot TrueNAS
|
||||
description: Reboot TrueNAS System (Target Uptime Sensor)
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: sensor
|
||||
|
||||
system_shutdown:
|
||||
name: Shutdown TrueNAS
|
||||
description: Shutdown TrueNAS System (Target Uptime Sensor)
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: sensor
|
||||
|
||||
service_start:
|
||||
name: Service Start
|
||||
description: Start a Service
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
service_stop:
|
||||
name: Service Stop
|
||||
description: Stop a Service
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
service_restart:
|
||||
name: Service Restart
|
||||
description: Restart a Service
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
service_reload:
|
||||
name: Service Reload
|
||||
description: Reload a Service
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
jail_start:
|
||||
name: Jail Start
|
||||
description: Start a Jail
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
jail_stop:
|
||||
name: Jail Stop
|
||||
description: Stop a Jail
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
jail_restart:
|
||||
name: Jail Restart
|
||||
description: Restart a Jail
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
vm_start:
|
||||
name: VM Start
|
||||
description: Start a VM
|
||||
fields:
|
||||
overcommit:
|
||||
name: Overcommit Memory
|
||||
description: Memory overcommitment allows the VM to launch even though there is insufficient free memory.
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
vm_stop:
|
||||
name: VM Stop
|
||||
description: Stop a VM
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
app_start:
|
||||
name: App Start
|
||||
description: Start a app
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
|
||||
app_stop:
|
||||
name: App Stop
|
||||
description: Stop a app
|
||||
target:
|
||||
entity:
|
||||
integration: truenas
|
||||
domain: binary_sensor
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up TrueNAS integration.",
|
||||
"data": {
|
||||
"name": "Name of the integration",
|
||||
"host": "Host",
|
||||
"api_key": "API key",
|
||||
"ssl": "Use SSL",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Name already exists.",
|
||||
"no_response": "No response from host.",
|
||||
"401": "No authorization for this endpoint.",
|
||||
"404": "API not found on this host.",
|
||||
"500": "Internal error."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up TrueNAS integration.",
|
||||
"data": {
|
||||
"name": "Name of the integration",
|
||||
"host": "Host",
|
||||
"api_key": "API key",
|
||||
"ssl": "Use SSL",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Name already exists.",
|
||||
"401": "No authorization for this endpoint.",
|
||||
"404": "API not found on this host.",
|
||||
"500": "Internal error.",
|
||||
"no_response": "No response from host."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Configure la integración de TrueNAS.",
|
||||
"data": {
|
||||
"name": "Nombre de la integración",
|
||||
"host": "Anfitrión",
|
||||
"api_key": "Clave API",
|
||||
"ssl": "Usar SSL",
|
||||
"verify_ssl": "Verificar certificado SSL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Nombre ya existente.",
|
||||
"401": "No hay autorización para este extremo.",
|
||||
"404": "API no encontrada en este host.",
|
||||
"500": "Error Interno.",
|
||||
"no_response": "No hay respuesta del anfitrión."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Configure a integração TrueNAS.",
|
||||
"data": {
|
||||
"name": "Nome da integração",
|
||||
"host": "Host",
|
||||
"api_key": "Chave API",
|
||||
"ssl": "Usar SSL",
|
||||
"verify_ssl": "Verificar certificado SSL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "O nome já existe.",
|
||||
"401": "Nenhuma autorização para este endpoint.",
|
||||
"404": "API não encontrada neste host.",
|
||||
"500": "Erro interno.",
|
||||
"no_response": "Sem resposta do host."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Настройка интеграции TrueNAS.",
|
||||
"data": {
|
||||
"name": "Название интеграции",
|
||||
"host": "Хост",
|
||||
"api_key": "Ключ API",
|
||||
"ssl": "Использовать SSL",
|
||||
"verify_ssl": "Проверка SSL-сертификата"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Имя уже используется",
|
||||
"401": "Ошибка авторизации",
|
||||
"404": "API не найден",
|
||||
"500": "Внутренняя ошибка",
|
||||
"no_response": "Хост не отвечает"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Nastavenie integrácie TrueNAS.",
|
||||
"data": {
|
||||
"name": "Názov integrácie",
|
||||
"host": "Hostiteľ",
|
||||
"api_key": "API kľúč",
|
||||
"ssl": "Použiť SSL",
|
||||
"verify_ssl": "Overte certifikát SSL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name_exists": "Názov už existuje.",
|
||||
"401": "Žiadna autorizácia pre tento koncový bod.",
|
||||
"404": "API sa na tomto hostiteľovi nenašlo.",
|
||||
"500": "Interný error.",
|
||||
"no_response": "Hostiteľ nereaguje."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
"""TrueNAS binary sensor platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from homeassistant.components.update import (
|
||||
UpdateDeviceClass,
|
||||
UpdateEntity,
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
|
||||
from .coordinator import TrueNASCoordinator
|
||||
from .entity import TrueNASEntity, async_add_entities
|
||||
from .update_types import SENSOR_SERVICES, SENSOR_TYPES
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
DEVICE_UPDATE = "device_update"
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# async_setup_entry
|
||||
# ---------------------------
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
_async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up device tracker for OpenMediaVault component."""
|
||||
dispatcher = {
|
||||
"TrueNASUpdate": TrueNASUpdate,
|
||||
}
|
||||
await async_add_entities(hass, config_entry, dispatcher)
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# TrueNASUpdate
|
||||
# ---------------------------
|
||||
class TrueNASUpdate(TrueNASEntity, UpdateEntity):
|
||||
"""Define an TrueNAS Update Sensor."""
|
||||
|
||||
TYPE = DEVICE_UPDATE
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TrueNASCoordinator,
|
||||
entity_description,
|
||||
uid: str | None = None,
|
||||
):
|
||||
"""Set up device update entity."""
|
||||
super().__init__(coordinator, entity_description, uid)
|
||||
|
||||
self._attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
self._attr_supported_features |= UpdateEntityFeature.PROGRESS
|
||||
self._attr_title = self.entity_description.title
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str:
|
||||
"""Version installed and in use."""
|
||||
return self._data["version"]
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str:
|
||||
"""Latest version available for install."""
|
||||
return self._data["update_version"]
|
||||
|
||||
async def options_updated(self) -> None:
|
||||
"""No action needed."""
|
||||
|
||||
async def async_install(self, version: str, backup: bool, **kwargs: Any) -> None:
|
||||
"""Install an update."""
|
||||
self._data["update_jobid"] = await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.query,
|
||||
"update/update",
|
||||
"post",
|
||||
{"reboot": True},
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def in_progress(self) -> int:
|
||||
"""Update installation progress."""
|
||||
if self._data["update_state"] != "RUNNING":
|
||||
return False
|
||||
|
||||
if self._data["update_progress"] == 0:
|
||||
self._data["update_progress"] = 1
|
||||
|
||||
return self._data["update_progress"]
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Definitions for TrueNAS update entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
from homeassistant.components.update import UpdateEntityDescription
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrueNASUpdateEntityDescription(UpdateEntityDescription):
|
||||
"""Class describing entities."""
|
||||
|
||||
ha_group: str | None = None
|
||||
ha_connection: str | None = None
|
||||
ha_connection_value: str | None = None
|
||||
title: str | None = None
|
||||
data_path: str | None = None
|
||||
data_attribute: str = "available"
|
||||
data_name: str | None = None
|
||||
data_uid: str | None = None
|
||||
data_reference: str | None = None
|
||||
data_attributes_list: List = field(default_factory=lambda: [])
|
||||
func: str = "TrueNASUpdate"
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[TrueNASUpdateEntityDescription, ...] = (
|
||||
TrueNASUpdateEntityDescription(
|
||||
key="system_update",
|
||||
name="Update",
|
||||
ha_group="System",
|
||||
title="TrueNAS",
|
||||
data_path="system_info",
|
||||
data_attribute="update_available",
|
||||
data_name="",
|
||||
data_uid="",
|
||||
data_reference="",
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_SERVICES = []
|
||||
Reference in New Issue
Block a user