diff --git a/custom_components/proxmoxve/__init__.py b/custom_components/proxmoxve/__init__.py index ca5f363..1d26684 100644 --- a/custom_components/proxmoxve/__init__.py +++ b/custom_components/proxmoxve/__init__.py @@ -43,6 +43,7 @@ CONF_NODES, CONF_QEMU, CONF_REALM, + CONF_STORAGE, CONF_VMS, COORDINATORS, DEFAULT_PORT, @@ -59,6 +60,7 @@ ProxmoxLXCCoordinator, ProxmoxNodeCoordinator, ProxmoxQEMUCoordinator, + ProxmoxStorageCoordinator, ) PLATFORMS = [ @@ -252,6 +254,25 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): remove_config_entry_id=config_entry.entry_id, ) + + if config_entry.version == 3: + config_entry.version = 4 + data_new = { + CONF_HOST: config_entry.data.get(CONF_HOST), + CONF_PORT: config_entry.data.get(CONF_PORT), + CONF_USERNAME: config_entry.data.get(CONF_USERNAME), + CONF_PASSWORD: config_entry.data.get(CONF_PASSWORD), + CONF_REALM: config_entry.data.get(CONF_REALM), + CONF_VERIFY_SSL: config_entry.data.get(CONF_VERIFY_SSL), + CONF_NODES: config_entry.data.get(CONF_NODES), + CONF_QEMU: config_entry.data.get(CONF_QEMU), + CONF_LXC: config_entry.data.get(CONF_LXC), + CONF_STORAGE: [], + } + hass.config_entries.async_update_entry( + config_entry, data=data_new, options={} + ) + LOGGER.info("Migration to version %s successful", config_entry.version) return True @@ -383,7 +404,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "platform": DOMAIN, "host": config_entry.data[CONF_HOST], "port": config_entry.data[CONF_PORT], - "resource_type": "QEMU", + "resource_type": ProxmoxType.QEMU, "resource": vm_id, }, ) @@ -419,11 +440,47 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b "platform": DOMAIN, "host": config_entry.data[CONF_HOST], "port": config_entry.data[CONF_PORT], - "resource_type": "LXC", + "resource_type": ProxmoxType.LXC, "resource": container_id, }, ) + for storage_id in config_entry.data[CONF_STORAGE]: + if storage_id in [ + (resource["storage"] if "storage" in resource else None) + for resource in resources + ]: + async_delete_issue( + hass, + DOMAIN, + f"{config_entry.entry_id}_{storage_id}_resource_nonexistent", + ) + coordinator_storage = ProxmoxStorageCoordinator( + hass=hass, + proxmox=proxmox, + api_category=ProxmoxType.Storage, + storage_id=storage_id, + ) + await coordinator_storage.async_refresh() + coordinators[storage_id] = coordinator_storage + else: + async_create_issue( + hass, + DOMAIN, + f"{config_entry.entry_id}_{storage_id}_resource_nonexistent", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="resource_nonexistent", + translation_placeholders={ + "integration": "Proxmox VE", + "platform": DOMAIN, + "host": config_entry.data[CONF_HOST], + "port": config_entry.data[CONF_PORT], + "resource_type": ProxmoxType.Storage, + "resource": storage_id, + }, + ) + hass.data[DOMAIN][config_entry.entry_id] = { PROXMOX_CLIENT: proxmox_client, COORDINATORS: coordinators, @@ -476,7 +533,7 @@ def device_info( config_entry: ConfigEntry, api_category: ProxmoxType, node: str | None = None, - vm_id: int | None = None, + resource_id: int | None = None, create: bool | None = False, ): """Return the Device Info.""" @@ -488,27 +545,41 @@ def device_info( proxmox_version = None if api_category in (ProxmoxType.QEMU, ProxmoxType.LXC): - coordinator = coordinators[vm_id] + coordinator = coordinators[resource_id] if (coordinator_data := coordinator.data) is not None: vm_name = coordinator_data.name node = coordinator_data.node - name = f"{api_category.upper()} {vm_name} ({vm_id})" - identifier = f"{config_entry.entry_id}_{api_category.upper()}_{vm_id}" - url = f"https://{host}:{port}/#v1:0:={api_category}/{vm_id}" + name = f"{api_category.upper()} {vm_name} ({resource_id})" + identifier = f"{config_entry.entry_id}_{api_category.upper()}_{resource_id}" + url = f"https://{host}:{port}/#v1:0:={api_category}/{resource_id}" via_device = ( DOMAIN, f"{config_entry.entry_id}_{ProxmoxType.Node.upper()}_{node}", ) model = api_category.upper() + elif api_category is ProxmoxType.Storage: + coordinator = coordinators[resource_id] + if (coordinator_data := coordinator.data) is not None: + node = coordinator_data.node + + name = f"{api_category.capitalize()} {resource_id}" + identifier = f"{config_entry.entry_id}_{api_category.upper()}_{resource_id}" + url = f"https://{host}:{port}/#v1:0:={api_category}/{node}/{resource_id}" + via_device = ( + DOMAIN, + f"{config_entry.entry_id}_{ProxmoxType.Node.upper()}_{node}", + ) + model = api_category.capitalize() + elif api_category is ProxmoxType.Node: coordinator = coordinators[node] if (coordinator_data := coordinator.data) is not None: model_processor = coordinator_data.model proxmox_version = f"Proxmox {coordinator_data.version}" - name = f"Node {node}" + name = f"{api_category.capitalize()} {node}" identifier = f"{config_entry.entry_id}_{api_category.upper()}_{node}" url = f"https://{host}:{port}/#v1:0:=node/{node}" via_device = ("", "") diff --git a/custom_components/proxmoxve/binary_sensor.py b/custom_components/proxmoxve/binary_sensor.py index 8e4b1e8..7476f97 100644 --- a/custom_components/proxmoxve/binary_sensor.py +++ b/custom_components/proxmoxve/binary_sensor.py @@ -107,7 +107,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.QEMU, - vm_id=vm_id, + resource_id=vm_id, ), description=description, resource_id=vm_id, @@ -129,7 +129,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.LXC, - vm_id=container_id, + resource_id=container_id, ), description=description, resource_id=container_id, diff --git a/custom_components/proxmoxve/button.py b/custom_components/proxmoxve/button.py index 653d6ff..d43463f 100644 --- a/custom_components/proxmoxve/button.py +++ b/custom_components/proxmoxve/button.py @@ -185,7 +185,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.QEMU, - vm_id=vm_id, + resource_id=vm_id, ), description=description, resource_id=vm_id, @@ -209,7 +209,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.LXC, - vm_id=ct_id, + resource_id=ct_id, ), description=description, resource_id=ct_id, diff --git a/custom_components/proxmoxve/config_flow.py b/custom_components/proxmoxve/config_flow.py index 296669e..b2cf37c 100644 --- a/custom_components/proxmoxve/config_flow.py +++ b/custom_components/proxmoxve/config_flow.py @@ -35,6 +35,7 @@ CONF_NODES, CONF_QEMU, CONF_REALM, + CONF_STORAGE, CONF_VMS, DEFAULT_PORT, DEFAULT_REALM, @@ -181,6 +182,11 @@ async def async_step_change_expose( for lxc in self.config_entry.data[CONF_LXC]: old_lxc.append(str(lxc)) + old_storage = [] + + for storage in self.config_entry.data[CONF_STORAGE]: + old_storage.append(str(storage)) + host = self.config_entry.data[CONF_HOST] port = self.config_entry.data[CONF_PORT] user = self.config_entry.data[CONF_USERNAME] @@ -217,6 +223,7 @@ async def async_step_change_expose( LOGGER.debug("Response API - Resources: %s", resources) resource_qemu = {} resource_lxc = {} + resource_storage = {} for resource in resources: if ("type" in resource) and (resource["type"] == ProxmoxType.Node): if resource["node"] not in resource_nodes: @@ -235,6 +242,13 @@ async def async_step_change_expose( ] = f"{resource['vmid']} {resource['name']}" else: resource_lxc[str(resource["vmid"])] = f"{resource['vmid']}" + if ("type" in resource) and (resource["type"] == ProxmoxType.Storage): + if "storage" in resource: + resource_storage[ + str(resource["storage"]) + ] = f"{resource['storage']} {resource['id']}" + else: + resource_storage[str(resource["storage"])] = f"{resource['storage']}" return self.async_show_form( step_id="change_expose", @@ -255,6 +269,12 @@ async def async_step_change_expose( **resource_lxc, } ), + vol.Optional(CONF_STORAGE, default=old_storage): cv.multi_select( + { + **dict.fromkeys(old_storage), + **resource_storage, + } + ), } ), ) @@ -313,11 +333,11 @@ async def async_step_change_expose( lxc_selecition = [] if ( - CONF_QEMU in user_input + CONF_LXC in user_input and (lxc_user := user_input.get(CONF_LXC)) is not None ): - for qemu in lxc_user: - lxc_selecition.append(qemu) + for lxc in lxc_user: + lxc_selecition.append(lxc) for lxc_id in self.config_entry.data[CONF_LXC]: if lxc_id not in lxc_selecition: @@ -334,11 +354,37 @@ async def async_step_change_expose( DOMAIN, f"{self.config_entry.entry_id}_{lxc_id}_resource_nonexistent", ) + + storage_selecition = [] + if ( + CONF_STORAGE in user_input + and (storage_user := user_input.get(CONF_STORAGE)) is not None + ): + for storage in storage_user: + storage_selecition.append(storage) + + for storage_id in self.config_entry.data[CONF_STORAGE]: + if storage_id not in storage_selecition: + # Remove device + identifier = ( + f"{self.config_entry.entry_id}_{ProxmoxType.Storage.upper()}_{storage_id}" + ) + await self.async_remove_device( + entry_id=self.config_entry.entry_id, + device_identifier=identifier, + ) + async_delete_issue( + self.hass, + DOMAIN, + f"{self.config_entry.entry_id}_{storage_id}_resource_nonexistent", + ) + config_data.update( { CONF_NODES: node_selecition, CONF_QEMU: qemu_selecition, CONF_LXC: lxc_selecition, + CONF_STORAGE: storage_selecition, } ) @@ -372,7 +418,7 @@ async def async_remove_device( class ProxmoxVEConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """ProxmoxVE Config Flow class.""" - VERSION = 3 + VERSION = 4 _reauth_entry: config_entries.ConfigEntry | None = None def __init__(self) -> None: @@ -722,6 +768,7 @@ async def async_step_expose( resource_nodes = [] resource_qemu = {} resource_lxc = {} + resource_storage = {} if resources is None: return self.async_abort(reason="no_resources") for resource in resources: @@ -742,6 +789,13 @@ async def async_step_expose( ] = f"{resource['vmid']} {resource['name']}" else: resource_lxc[str(resource["vmid"])] = f"{resource['vmid']}" + if ("type" in resource) and (resource["type"] == ProxmoxType.Storage): + if "storage" in resource: + resource_storage[ + str(resource["storage"]) + ] = f"{resource['storage']} {resource['id']}" + else: + resource_lxc[str(resource["storage"])] = f"{resource['storage']}" return self.async_show_form( step_id="expose", @@ -750,6 +804,7 @@ async def async_step_expose( vol.Required(CONF_NODES): cv.multi_select(resource_nodes), vol.Optional(CONF_QEMU): cv.multi_select(resource_qemu), vol.Optional(CONF_LXC): cv.multi_select(resource_lxc), + vol.Optional(CONF_STORAGE): cv.multi_select(resource_storage), } ), ) @@ -781,6 +836,15 @@ async def async_step_expose( for lxc_selection in lxc_user: self._config[CONF_LXC].append(lxc_selection) + if CONF_STORAGE not in self._config: + self._config[CONF_STORAGE] = [] + if ( + CONF_STORAGE in user_input + and (storage_user := user_input.get(CONF_STORAGE)) is not None + ): + for storage_selection in storage_user: + self._config[CONF_STORAGE].append(storage_selection) + return self.async_create_entry( title=(f"{self._config[CONF_HOST]}:{self._config[CONF_PORT]}"), data=self._config, diff --git a/custom_components/proxmoxve/const.py b/custom_components/proxmoxve/const.py index 89a912e..a0b76cf 100644 --- a/custom_components/proxmoxve/const.py +++ b/custom_components/proxmoxve/const.py @@ -28,6 +28,7 @@ CONF_QEMU = "qemu" CONF_REALM = "realm" CONF_VMS = "vms" +CONF_STORAGE="storage" PROXMOX_CLIENT = "proxmox_client" @@ -41,6 +42,7 @@ class ProxmoxType(StrEnum): Node = "node" QEMU = "qemu" LXC = "lxc" + Storage = "storage" class ProxmoxCommand(StrEnum): diff --git a/custom_components/proxmoxve/coordinator.py b/custom_components/proxmoxve/coordinator.py index 4e93d4b..4e4c2e8 100644 --- a/custom_components/proxmoxve/coordinator.py +++ b/custom_components/proxmoxve/coordinator.py @@ -21,7 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_NODE, DOMAIN, LOGGER, UPDATE_INTERVAL, ProxmoxType -from .models import ProxmoxLXCData, ProxmoxNodeData, ProxmoxVMData +from .models import ProxmoxLXCData, ProxmoxNodeData, ProxmoxStorageData, ProxmoxVMData class ProxmoxCoordinator( @@ -52,7 +52,6 @@ def __init__( self.hass = hass self.config_entry: ConfigEntry = self.config_entry self.proxmox = proxmox - self.node_name = node_name self.resource_id = node_name async def _async_update_data(self) -> ProxmoxNodeData: @@ -61,16 +60,16 @@ async def _async_update_data(self) -> ProxmoxNodeData: def poll_api() -> dict[str, Any] | None: """Return data from the Proxmox Node API.""" try: - api_status = self.proxmox.nodes(self.node_name).status.get() + api_status = self.proxmox.nodes(self.resource_id).status.get() if nodes_api := self.proxmox.nodes.get(): for node_api in nodes_api: - if node_api[CONF_NODE] == self.node_name: + if node_api[CONF_NODE] == self.resource_id: api_status["status"] = node_api["status"] api_status["cpu"] = node_api["cpu"] api_status["disk_max"] = node_api["maxdisk"] api_status["disk_used"] = node_api["disk"] break - api_status["version"] = self.proxmox.nodes(self.node_name).version.get() + api_status["version"] = self.proxmox.nodes(self.resource_id).version.get() except ( AuthenticationError, @@ -83,12 +82,12 @@ def poll_api() -> dict[str, Any] | None: async_create_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.node_name}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", is_fixable=False, severity=IssueSeverity.ERROR, translation_key="resource_exception_forbiden", translation_placeholders={ - "resource": f"Node {self.node_name}", + "resource": f"Node {self.resource_id}", "user": self.config_entry.data[CONF_USERNAME], }, ) @@ -99,7 +98,7 @@ def poll_api() -> dict[str, Any] | None: async_delete_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.node_name}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", ) LOGGER.debug("API Response - Node: %s", api_status) @@ -109,11 +108,11 @@ def poll_api() -> dict[str, Any] | None: if api_status is None: raise UpdateFailed( - f"Node {self.node_name} unable to be found in host {self.config_entry.data[CONF_HOST]}" + f"Node {self.resource_id} unable to be found in host {self.config_entry.data[CONF_HOST]}" ) return ProxmoxNodeData( - type="NODE", + type=ProxmoxType.Node, model=api_status["cpuinfo"]["model"], status=api_status["status"], version=api_status["version"]["version"], @@ -153,7 +152,6 @@ def __init__( self.config_entry: ConfigEntry = self.config_entry self.proxmox = proxmox self.node_name: str - self.vm_id = qemu_id self.resource_id = qemu_id async def _async_update_data(self) -> ProxmoxVMData: @@ -170,17 +168,20 @@ def poll_api() -> dict[str, Any] | None: for resource in resources: if "vmid" in resource: - if int(resource["vmid"]) == int(self.vm_id): + if int(resource["vmid"]) == int(self.resource_id): node_name = resource["node"] self.node_name = str(node_name) if self.node_name is not None: api_status = ( self.proxmox.nodes(self.node_name) - .qemu(self.vm_id) + .qemu(self.resource_id) .status.current.get() ) + if node_name is None: + raise UpdateFailed(f"{self.resource_id} QEMU node not found") + except ( AuthenticationError, SSLError, @@ -192,12 +193,12 @@ def poll_api() -> dict[str, Any] | None: async_create_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.vm_id}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", is_fixable=False, severity=IssueSeverity.ERROR, translation_key="resource_exception_forbiden", translation_placeholders={ - "resource": f"QEMU {self.vm_id}", + "resource": f"QEMU {self.resource_id}", "user": self.config_entry.data[CONF_USERNAME], }, ) @@ -208,7 +209,7 @@ def poll_api() -> dict[str, Any] | None: async_delete_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.vm_id}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", ) LOGGER.debug("API Response - QEMU: %s", api_status) @@ -217,11 +218,11 @@ def poll_api() -> dict[str, Any] | None: api_status = await self.hass.async_add_executor_job(poll_api) if api_status is None or "status" not in api_status: - raise UpdateFailed(f"Vm/Container {self.vm_id} unable to be found") + raise UpdateFailed(f"Vm/Container {self.resource_id} unable to be found") update_device_via(self, ProxmoxType.QEMU) return ProxmoxVMData( - type="QEMU", + type=ProxmoxType.QEMU, status=api_status["status"], name=api_status["name"], node=self.node_name, @@ -261,7 +262,6 @@ def __init__( self.config_entry: ConfigEntry = self.config_entry self.proxmox = proxmox self.node_name: str - self.vm_id = container_id self.resource_id = container_id async def _async_update_data(self) -> ProxmoxLXCData: @@ -278,17 +278,20 @@ def poll_api() -> dict[str, Any] | None: for resource in resources: if "vmid" in resource: - if int(resource["vmid"]) == int(self.vm_id): + if int(resource["vmid"]) == int(self.resource_id): node_name = resource["node"] self.node_name = str(node_name) if node_name is not None: api_status = ( self.proxmox.nodes(self.node_name) - .lxc(self.vm_id) + .lxc(self.resource_id) .status.current.get() ) + if node_name is None: + raise UpdateFailed(f"{self.resource_id} LXC node not found") + except ( AuthenticationError, SSLError, @@ -300,12 +303,12 @@ def poll_api() -> dict[str, Any] | None: async_create_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.vm_id}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", is_fixable=False, severity=IssueSeverity.ERROR, translation_key="resource_exception_forbiden", translation_placeholders={ - "resource": f"LXC {self.node_name}", + "resource": f"LXC {self.resource_id}", "user": self.config_entry.data[CONF_USERNAME], }, ) @@ -316,7 +319,7 @@ def poll_api() -> dict[str, Any] | None: async_delete_issue( self.hass, DOMAIN, - f"{self.config_entry.entry_id}_{self.vm_id}_forbiden", + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", ) LOGGER.debug("API Response - LXC: %s", api_status) @@ -325,11 +328,11 @@ def poll_api() -> dict[str, Any] | None: api_status = await self.hass.async_add_executor_job(poll_api) if api_status is None or "status" not in api_status: - raise UpdateFailed(f"Vm/Container {self.vm_id} unable to be found") + raise UpdateFailed(f"Vm/Container {self.resource_id} unable to be found") update_device_via(self, ProxmoxType.LXC) return ProxmoxLXCData( - type="LXC", + type=ProxmoxType.LXC, status=api_status["status"], name=api_status["name"], node=self.node_name, @@ -348,6 +351,108 @@ def poll_api() -> dict[str, Any] | None: ) +class ProxmoxStorageCoordinator(ProxmoxCoordinator): + """Proxmox VE Storage data update coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + proxmox: ProxmoxAPI, + api_category: str, + storage_id: int, + ) -> None: + """Initialize the Proxmox LXC coordinator.""" + + super().__init__( + hass, + LOGGER, + name=f"proxmox_coordinator_{api_category}_{storage_id}", + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + self.hass = hass + self.config_entry: ConfigEntry = self.config_entry + self.proxmox = proxmox + self.node_name: str + self.resource_id = storage_id + + async def _async_update_data(self) -> ProxmoxLXCData: + """Update data for Proxmox LXC.""" + + def poll_api() -> dict[str, Any] | None: + """Return data from the Proxmox LXC API.""" + node_name = None + try: + api_status = None + + resources = self.proxmox.cluster.resources.get() + LOGGER.debug("API Response - Resources: %s", resources) + + for resource in resources: + if "storage" in resource: + if resource["storage"] == self.resource_id: + node_name = resource["node"] + + self.node_name = str(node_name) + if node_name is not None: + api_status = ( + self.proxmox.nodes(self.node_name) + .storage(self.resource_id) + .status.get() + ) + + if node_name is None: + raise UpdateFailed(f"{self.resource_id} storage node not found") + + except ( + AuthenticationError, + SSLError, + ConnectTimeout, + ) as error: + raise UpdateFailed(error) from error + except ResourceException as error: + if error.status_code == 403: + async_create_issue( + self.hass, + DOMAIN, + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="resource_exception_forbiden", + translation_placeholders={ + "resource": f"Storage {self.resource_id}", + "user": self.config_entry.data[CONF_USERNAME], + }, + ) + raise UpdateFailed( + "User not allowed to access the resource, check user permissions as per the documentation." + ) from error + + async_delete_issue( + self.hass, + DOMAIN, + f"{self.config_entry.entry_id}_{self.resource_id}_forbiden", + ) + + LOGGER.debug("API Response - Storage: %s", api_status) + return api_status + + api_status = await self.hass.async_add_executor_job(poll_api) + + #if api_status is None or "content" not in api_status: + # raise UpdateFailed(f"Storage {self.resource_id} unable to be found") + + update_device_via(self, ProxmoxType.Storage) + return ProxmoxStorageData( + type=ProxmoxType.Storage, + node=self.node_name, + disk_total=api_status["total"], + disk_used=api_status["used"], + disk_free=api_status["avail"], + content=api_status["content"] + ) + + def update_device_via( self, api_category: ProxmoxType, @@ -359,7 +464,7 @@ def update_device_via( identifiers={ ( DOMAIN, - f"{self.config_entry.entry_id}_{api_category.upper()}_{self.vm_id}", + f"{self.config_entry.entry_id}_{api_category.upper()}_{self.resource_id}", ) }, ) @@ -375,7 +480,7 @@ def update_device_via( if device.via_device_id != via_device_id: LOGGER.debug( "Update device %s - connected via device: old=%s, new=%s", - self.vm_id, + self.resource_id, device.via_device_id, via_device_id, ) diff --git a/custom_components/proxmoxve/manifest.json b/custom_components/proxmoxve/manifest.json index 070966c..68d23f5 100644 --- a/custom_components/proxmoxve/manifest.json +++ b/custom_components/proxmoxve/manifest.json @@ -8,5 +8,5 @@ "issue_tracker": "https://github.com/dougiteixeira/proxmoxve/issues", "loggers": ["proxmoxer"], "requirements": ["proxmoxer==2.0.1"], - "version": "2.1.1" + "version": "2.2.0" } diff --git a/custom_components/proxmoxve/models.py b/custom_components/proxmoxve/models.py index 664df05..200e8b5 100644 --- a/custom_components/proxmoxve/models.py +++ b/custom_components/proxmoxve/models.py @@ -104,3 +104,16 @@ class ProxmoxLXCData: swap_free: float swap_used: float uptime: int + + + +@dataclasses.dataclass +class ProxmoxStorageData: + """Data parsed from the Proxmox API for Storage.""" + + type: str + node: str + content: str + disk_free: float + disk_used: float + disk_total: float \ No newline at end of file diff --git a/custom_components/proxmoxve/sensor.py b/custom_components/proxmoxve/sensor.py index 2704a77..3e82b36 100644 --- a/custom_components/proxmoxve/sensor.py +++ b/custom_components/proxmoxve/sensor.py @@ -26,6 +26,7 @@ CONF_LXC, CONF_NODES, CONF_QEMU, + CONF_STORAGE, COORDINATORS, DOMAIN, ProxmoxKeyAPIParse, @@ -318,6 +319,14 @@ class ProxmoxSensorEntityDescription(ProxmoxEntityDescription, SensorEntityDescr *PROXMOX_SENSOR_UPTIME, ) +PROXMOX_SENSOR_STORAGE: Final[tuple[ProxmoxSensorEntityDescription, ...]] = ( + ProxmoxSensorEntityDescription( + key="node", + name="Node", + icon="mdi:server", + ), + *PROXMOX_SENSOR_DISK, +) async def async_setup_entry( hass: HomeAssistant, @@ -364,7 +373,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.QEMU, - vm_id=vm_id, + resource_id=vm_id, ), description=description, resource_id=vm_id, @@ -386,7 +395,7 @@ async def async_setup_entry( hass=hass, config_entry=config_entry, api_category=ProxmoxType.LXC, - vm_id=ct_id, + resource_id=ct_id, ), description=description, resource_id=ct_id, @@ -394,6 +403,28 @@ async def async_setup_entry( ) ) + for storage_id in config_entry.data[CONF_STORAGE]: + coordinator = coordinators[storage_id] + # unfound container case + if coordinator.data is None: + continue + for description in PROXMOX_SENSOR_STORAGE: + if description.api_category in (None, ProxmoxType.Storage): + sensors.append( + create_sensor( + coordinator=coordinator, + info_device=device_info( + hass=hass, + config_entry=config_entry, + api_category=ProxmoxType.Storage, + resource_id=storage_id, + ), + description=description, + resource_id=storage_id, + config_entry=config_entry, + ) + ) + async_add_entities(sensors) diff --git a/custom_components/proxmoxve/strings.json b/custom_components/proxmoxve/strings.json index 6220306..ef64dab 100644 --- a/custom_components/proxmoxve/strings.json +++ b/custom_components/proxmoxve/strings.json @@ -17,7 +17,8 @@ "data": { "nodes": "Nodes", "qemu": "Virtual Machines (QEMU)", - "lxc": "Linux Containers (LXC)" + "lxc": "Linux Containers (LXC)", + "storage": "Storages" } }, "reauth_confirm": { @@ -97,7 +98,7 @@ "menu": { "menu_options": { "host_auth": "Change host authentication information", - "change_expose": "Add or remove Nodes, VMs or Containers" + "change_expose": "Add or remove Nodes, VMs, Containers or Storages" } }, "host_auth": { @@ -114,7 +115,8 @@ "data": { "nodes": "[%key:component::proxmoxve::config::step::expose::data::nodes%]", "qemu": "[%key:component::proxmoxve::config::step::expose::data::qemu%]", - "lxc": "[%key:component::proxmoxve::config::step::expose::data::lxc%]" + "lxc": "[%key:component::proxmoxve::config::step::expose::data::lxc%]", + "storage": "[%key:component::proxmoxve::config::step::expose::data::storage%]" } } }, diff --git a/custom_components/proxmoxve/translations/en.json b/custom_components/proxmoxve/translations/en.json index d48b864..0a4831a 100644 --- a/custom_components/proxmoxve/translations/en.json +++ b/custom_components/proxmoxve/translations/en.json @@ -1,240 +1,239 @@ { "config": { - "abort": { - "already_configured": "Device is already configured", - "no_resources": "No resources were returned for this instance.", - "reauth_successful": "Re-authentication was successful" - }, - "error": { - "auth_error": "Invalid authentication", - "cant_connect": "Failed to connect", - "general_error": "Unexpected error", - "invalid_port": "Invalid port number", - "ssl_rejection": "Could not verify the SSL certificate" - }, - "step": { - "expose": { - "data": { - "lxc": "Linux Containers (LXC)", - "nodes": "Nodes", - "qemu": "Virtual Machines (QEMU)" - }, - "description": "Select the Proxmox instance nodes ans Virtual Machines (QEMU) and Containers (LXC) you want to expose" - }, - "host": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "realm": "Realm", - "username": "Username", - "verify_ssl": "Verify SSL certificate" - }, - "description": "Proxmox host information" - }, - "reauth_confirm": { - "data": { - "password": "Password", - "username": "Username" - }, - "description": "The username or password is invalid.", - "title": "Reauthenticate Integration" - } + "step": { + "host": { + "description": "Proxmox host information", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "realm": "Realm", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "expose": { + "description": "Select the Proxmox instance nodes ans Virtual Machines (QEMU) and Containers (LXC) you want to expose", + "data": { + "nodes": "Nodes", + "qemu": "Virtual Machines (QEMU)", + "lxc": "Linux Containers (LXC)", + "storage": "Storages" + } + }, + "reauth_confirm": { + "description": "The username or password is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } + }, + "error": { + "auth_error": "[%key:common::config_flow::error::invalid_auth%]", + "ssl_rejection": "Could not verify the SSL certificate", + "cant_connect": "[%key:common::config_flow::error::cannot_connect%]", + "general_error": "[%key:common::config_flow::error::unknown%]", + "invalid_port": "Invalid port number" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "no_resources": "No resources were returned for this instance." + } }, - "entity": { - "binary_sensor": { - "health": { - "name": "Health" - }, - "status": { - "name": "Status" - } - }, - "button": { - "reboot": { - "name": "Reboot" - }, - "reset": { - "name": "Reset" - }, - "resume": { - "name": "Resume" - }, - "shutdown": { - "name": "Shutdown" - }, - "start": { - "name": "Start" - }, - "start_all": { - "name": "Start all" - }, - "stop": { - "name": "Stop" - }, - "stop_all": { - "name": "Stop all" - }, - "suspend": { - "name": "Suspend" - }, - "hibernate": { - "name": "Hibernate" - } - }, - "sensor": { - "cpu_used": { - "name": "CPU used" - }, - "disk_free": { - "name": "Disk free" - }, - "disk_free_perc": { - "name": "Disk free percentage" - }, - "disk_total": { - "name": "Disk total" - }, - "disk_used": { - "name": "Disk used" - }, - "disk_used_perc": { - "name": "Disk used percentage" - }, - "memory_free": { - "name": "Memory free" - }, - "memory_free_perc": { - "name": "Memory free percentage" - }, - "memory_total": { - "name": "Memory total" - }, - "memory_used": { - "name": "Memory used" - }, - "memory_used_perc": { - "name": "Memory used percentage" - }, - "network_in": { - "name": "Network in" - }, - "network_out": { - "name": "Network out" - }, - "node": { - "name": "Node" - }, - "swap_free": { - "name": "Swap free" - }, - "swap_free_perc": { - "name": "Swap free percentage" - }, - "swap_total": { - "name": "Swap total" - }, - "swap_used": { - "name": "Swap used" - }, - "swap_used_perc": { - "name": "Swap used percentage" - }, - "uptime": { - "name": "Uptime" - } + "issues": { + "import_success": { + "title": "{host}:{port} was imported from YAML configuration", + "description": "The YAML configuration of {host}:{port} instance of {integration} (`{platform}`) has been imported into the UI automatically.\n\nCan be safely removed from your `configuration.yaml` file." + }, + "import_invalid_port": { + "title": "Error in importing YAML configuration from {host}:{port}", + "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to invalid port.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually." + }, + "import_auth_error": { + "title": "Error in importing YAML configuration from {host}:{port}", + "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to authentication error.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually." + }, + "import_ssl_rejection": { + "title": "Error in importing YAML configuration from {host}:{port}", + "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to SSL rejection.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually." + }, + "import_cant_connect": { + "title": "Error in importing YAML configuration from {host}:{port}", + "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to connection failed.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually." + }, + "import_general_error": { + "title": "Error in importing YAML configuration from {host}:{port}", + "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to unknown error.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually." + }, + "import_already_configured": { + "title": "The instance {host}:{port} already exists in the UI, can be removed", + "description": "The YAML configuration of instace {host}:{port} of {integration} (`{platform}`) already exists in the UI and was ignored on import.\n\nYou must remove this configuration from your `configuration.yaml` file and restart Home Assistant." + }, + "import_node_not_exist": { + "title": "Node {node} does not exist in {host}:{port}", + "description": "The {node} of the {host}:{port} instance of {integration} (`{platform}`) present in the YAML configuration does not exist in this instance and was ignored in the import.\n\nYou must remove this configuration from your `configuration.yaml` file and restart Home Assistant." + }, + "yaml_deprecated": { + "title": "Configuration of the {integration} in YAML is deprecated", + "description": "Configuration of the {integration} (`{platform}`) in YAML is deprecated and should be removed in {version}.\n\nResolve the import issues and remove the YAML configuration from your `configuration.yaml` file." + }, + "resource_nonexistent": { + "description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.", + "title": "{resource_type} {resource} does not exist" + }, + "no_permissions": { + "description": "The user `{user}` does not have the required permissions for all features.\n\nThe following features are not accessible by the user:\n`{errors}`\n\nCheck the user permissions as described in the documentation.", + "title": "User `{user}` does not have the required permissions" + }, + "resource_exception_forbiden": { + "description": "User {user} does not have sufficient permissions to access resource {resource}.\n\nPlease check documentation and user permissions.", + "title": "Permissions error for `{resource}`" + } + }, + "options": { + "step": { + "menu": { + "menu_options": { + "host_auth": "Change host authentication information", + "change_expose": "Add or remove Nodes, VMs, Containers or Storages" + } + }, + "host_auth": { + "description": "[%key:component::proxmoxve::config::step::host::description%]", + "data": { + "username": "[%key:component::proxmoxve::config::step::host::data::username%]", + "password": "[%key:component::proxmoxve::config::step::host::data::password%]", + "realm": "[%key:component::proxmoxve::config::step::host::data::realm%]", + "verify_ssl": "[%key:component::proxmoxve::config::step::host::data::verify_ssl%]" + } + }, + "change_expose": { + "description": "[%key:component::proxmoxve::config::step::expose::description%]", + "data": { + "nodes": "[%key:component::proxmoxve::config::step::expose::data::nodes%]", + "qemu": "[%key:component::proxmoxve::config::step::expose::data::qemu%]", + "lxc": "[%key:component::proxmoxve::config::step::expose::data::lxc%]", + "storage": "[%key:component::proxmoxve::config::step::expose::data::storage%]" + } } + }, + "error": { + "auth_error": "[%key:component::proxmoxve::config::error::auth_error%]", + "ssl_rejection": "[%key:component::proxmoxve::config::error::ssl_rejection%]", + "cant_connect": "[%key:component::proxmoxve::config::error::cant_connect%]", + "general_error": "[%key:component::proxmoxve::config::error::general_error%]", + "invalid_port": "[%key:component::proxmoxve::config::error::invalid_port%]" + }, + "abort": { + "no_nodes": "No nodes were returned for the host.", + "no_vms": "There are no virtual machines or containers for this node, the configuration entry will be created for the node.", + "changes_successful": "Changes saved successfully.", + "no_nodes_to_add": "No nodes to add.", + "node_already_exists": "The selected node already exists." + } }, - "issues": { - "import_already_configured": { - "description": "The YAML configuration of instace {host}:{port} of {integration} (`{platform}`) already exists in the UI and was ignored on import.\n\nYou must remove this configuration from your `configuration.yaml` file and restart Home Assistant.", - "title": "The instance {host}:{port} already exists in the UI, can be removed" + "entity": { + "binary_sensor": { + "status": { + "name": "Status" + }, + "health": { + "name": "Health" + } + }, + "button": { + "start_all": { + "name": "Start all" }, - "import_auth_error": { - "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to authentication error.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually.", - "title": "Error in importing YAML configuration from {host}:{port}" + "stop_all": { + "name": "Stop all" }, - "import_cant_connect": { - "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to connection failed.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually.", - "title": "Error in importing YAML configuration from {host}:{port}" + "shutdown": { + "name": "Shutdown" }, - "import_general_error": { - "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to unknown error.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually.", - "title": "Error in importing YAML configuration from {host}:{port}" + "reboot": { + "name": "Reboot" }, - "import_invalid_port": { - "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to invalid port.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually.", - "title": "Error in importing YAML configuration from {host}:{port}" + "start": { + "name": "Start" }, - "import_node_not_exist": { - "description": "The {node} of the {host}:{port} instance of {integration} (`{platform}`) present in the YAML configuration does not exist in this instance and was ignored in the import.\n\nYou must remove this configuration from your `configuration.yaml` file and restart Home Assistant.", - "title": "Node {node} does not exist in {host}:{port}" + "stop": { + "name": "Stop" }, - "import_ssl_rejection": { - "description": "Importing YAML configuration from {host}:{port} instance of {integration} (`{platform}`) failed due to SSL rejection.\n\nYou must remove this configuration from your `configuration.yaml` file, restart Home Assistant and configure it manually.", - "title": "Error in importing YAML configuration from {host}:{port}" + "resume": { + "name": "Resume" }, - "import_success": { - "description": "The YAML configuration of {host}:{port} instance of {integration} (`{platform}`) has been imported into the UI automatically.\n\nCan be safely removed from your `configuration.yaml` file.", - "title": "{host}:{port} was imported from YAML configuration" + "suspend": { + "name": "Suspend" }, - "no_permissions": { - "description": "The user `{user}` does not have the required permissions for all features.\n\nThe following features are not accessible by the user:\n`{errors}`\n\nCheck the user permissions as described in the documentation.", - "title": "User `{user}` does not have the required permissions" + "reset": { + "name": "Reset" + } + }, + "sensor": { + "cpu_used": { + "name": "CPU used" }, - "resource_exception_forbiden": { - "description": "User {user} does not have sufficient permissions to access resource {resource}.\n\nPlease check documentation and user permissions.", - "title": "Permissions error for `{resource}`" + "disk_free": { + "name": "Disk free" }, - "resource_nonexistent": { - "description": "{resource_type} {resource} does not exist on ({host}:{port}), remove it in integration options.\n\nThis can also be caused if the user doesn't have enough permission to access the resource.", - "title": "{resource_type} {resource} does not exist" + "disk_free_perc": { + "name": "Disk free percentage" }, - "yaml_deprecated": { - "description": "Configuration of the {integration} (`{platform}`) in YAML is deprecated and should be removed in {version}.\n\nResolve the import issues and remove the YAML configuration from your `configuration.yaml` file.", - "title": "Configuration of the {integration} in YAML is deprecated" - } - }, - "options": { - "abort": { - "changes_successful": "Changes saved successfully.", - "no_nodes": "No nodes were returned for the host.", - "no_nodes_to_add": "No nodes to add.", - "no_vms": "There are no virtual machines or containers for this node, the configuration entry will be created for the node.", - "node_already_exists": "The selected node already exists." - }, - "error": { - "auth_error": "Invalid authentication", - "cant_connect": "Failed to connect", - "general_error": "Unexpected error", - "invalid_port": "Invalid port number", - "ssl_rejection": "Could not verify the SSL certificate" - }, - "step": { - "change_expose": { - "data": { - "lxc": "Linux Containers (LXC)", - "nodes": "Nodes", - "qemu": "Virtual Machines (QEMU)" - }, - "description": "Select the Proxmox instance nodes ans Virtual Machines (QEMU) and Containers (LXC) you want to expose" - }, - "host_auth": { - "data": { - "password": "Password", - "realm": "Realm", - "username": "Username", - "verify_ssl": "Verify SSL certificate" - }, - "description": "Proxmox host information" - }, - "menu": { - "menu_options": { - "change_expose": "Add or remove Nodes, VMs or Containers", - "host_auth": "Change host authentication information" - } - } + "disk_total": { + "name": "Disk total" + }, + "disk_used": { + "name": "Disk used" + }, + "disk_used_perc": { + "name": "Disk used percentage" + }, + "memory_free": { + "name": "Memory free" + }, + "memory_free_perc": { + "name": "Memory free percentage" + }, + "memory_total": { + "name": "Memory total" + }, + "memory_used": { + "name": "Memory used" + }, + "memory_used_perc": { + "name": "Memory used percentage" + }, + "network_in": { + "name": "Network in" + }, + "network_out": { + "name": "Network out" + }, + "node": { + "name": "Node" + }, + "swap_free": { + "name": "Swap free" + }, + "swap_free_perc": { + "name": "Swap free percentage" + }, + "swap_total": { + "name": "Swap total" + }, + "swap_used": { + "name": "Swap used" + }, + "swap_used_perc": { + "name": "Swap used percentage" + }, + "uptime": { + "name": "Uptime" } + } } -} \ No newline at end of file + } diff --git a/custom_components/proxmoxve/translations/pt-BR.json b/custom_components/proxmoxve/translations/pt-BR.json index fc66a07..3463d90 100644 --- a/custom_components/proxmoxve/translations/pt-BR.json +++ b/custom_components/proxmoxve/translations/pt-BR.json @@ -17,7 +17,8 @@ "data": { "lxc": "Contêiner Linux (LXC)", "node": "Nó", - "qemu": "Maquina Virtual (QEMU)" + "qemu": "Maquina Virtual (QEMU)", + "storage": "Armazenamentos" }, "description": "Selecione os nós da instância Proxmox, as máquinas virtuais (QEMU) e contêineres (LXC) que deseja expor" }, @@ -206,7 +207,8 @@ "data": { "lxc": "Contêiner Linux (LXC)", "node": "Nó", - "qemu": "Maquina Virtual (QEMU)" + "qemu": "Maquina Virtual (QEMU)", + "storage": "Armazenamentos" }, "description": "Selecione os nós da instância Proxmox, as máquinas virtuais (QEMU) e contêineres (LXC) que deseja expor" }, @@ -221,7 +223,7 @@ }, "menu": { "menu_options": { - "change_expose": "Adicionar ou remover Nós, VMs ou Contêineres", + "change_expose": "Adicionar ou remover Nós, VMs, Contêineres ou Armazenamentos", "host_auth": "Alterar as informações de autenticação do host" } }