Initial Commit

This commit is contained in:
2026-06-11 11:50:50 -04:00
commit d4a69c41be
2748 changed files with 80489 additions and 0 deletions
+56
View File
@@ -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
+144
View File
@@ -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
+353
View File
@@ -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
+352
View File
@@ -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"],
]
+123
View File
@@ -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,
)
+80
View File
@@ -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
+26
View File
@@ -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
),
}
+231
View File
@@ -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()
+19
View File
@@ -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
+14
View File
@@ -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"
}
+176
View File
@@ -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,
)
+445
View File
@@ -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"],
]
+137
View File
@@ -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
+23
View File
@@ -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."
}
}
}
+95
View File
@@ -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"]
+42
View File
@@ -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 = []