From 86a7e84ac03bfba23551f642551bcbbdb6791bfc Mon Sep 17 00:00:00 2001 From: Konstantin Vorobyev Date: Sun, 21 Jan 2024 14:56:36 +0100 Subject: [PATCH 1/2] Add openwrt.ubus service call --- custom_components/openwrt/__init__.py | 45 ++++++++++++++++++------ custom_components/openwrt/config_flow.py | 3 ++ custom_components/openwrt/coordinator.py | 31 +++++++++++++--- custom_components/openwrt/services.yaml | 35 +++++++++++++++++- custom_components/openwrt/ubus.py | 3 ++ 5 files changed, 100 insertions(+), 17 deletions(-) diff --git a/custom_components/openwrt/__init__.py b/custom_components/openwrt/__init__.py index 03896cd..fe3e346 100644 --- a/custom_components/openwrt/__init__.py +++ b/custom_components/openwrt/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations from .constants import DOMAIN, PLATFORMS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.helpers.typing import ConfigType from homeassistant.helpers import service from homeassistant.helpers.update_coordinator import ( @@ -53,15 +53,23 @@ async def async_reboot(call): async def async_exec(call): parts = call.data["command"].split(" ") - for entry_id in await service.async_extract_config_entry_ids(hass, call): - device = hass.data[DOMAIN]["devices"][entry_id] - if device.is_api_supported("file"): - await device.do_file_exec( - parts[0], - parts[1:], - call.data.get("environment", {}), - call.data.get("extra", {}) - ) + ids = await service.async_extract_config_entry_ids(hass, call) + response = {} + for entry_id in ids: + if coordinator := hass.data[DOMAIN]["devices"].get(entry_id): + if coordinator.is_api_supported("file"): + args = parts[1:] + if "arguments" in call.data: + args = call.data["arguments"].strip().split("\n") + response[entry_id] = await coordinator.do_file_exec( + parts[0], + args, + call.data.get("environment", {}), + call.data.get("extra", {}) + ) + if len(ids) == 1: + return response.get(list(ids)[0]) + return response async def async_init(call): parts = call.data["name"].split(" ") @@ -73,9 +81,24 @@ async def async_init(call): call.data.get("action", {}) ) + async def async_ubus(call): + response = {} + ids = await service.async_extract_config_entry_ids(hass, call) + for entry_id in ids: + if coordinator := hass.data[DOMAIN]["devices"].get(entry_id): + response[entry_id] = await coordinator.do_ubus_call( + call.data.get("subsystem"), + call.data.get("method"), + call.data.get("parameters", {}), + ) + if len(ids) == 1: + return response.get(list(ids)[0]) + return response + hass.services.async_register(DOMAIN, "reboot", async_reboot) - hass.services.async_register(DOMAIN, "exec", async_exec) + hass.services.async_register(DOMAIN, "exec", async_exec, supports_response=SupportsResponse.OPTIONAL) hass.services.async_register(DOMAIN, "init", async_init) + hass.services.async_register(DOMAIN, "ubus", async_ubus, supports_response=SupportsResponse.ONLY) return True diff --git a/custom_components/openwrt/config_flow.py b/custom_components/openwrt/config_flow.py index cdef375..6312d27 100644 --- a/custom_components/openwrt/config_flow.py +++ b/custom_components/openwrt/config_flow.py @@ -1,6 +1,7 @@ from homeassistant import config_entries import homeassistant.helpers.config_validation as cv from .constants import DOMAIN +from .coordinator import new_ubus_client import logging import voluptuous as vol @@ -36,5 +37,7 @@ async def async_step_user(self, user_input): _LOGGER.debug(f"Input: {user_input}") await self.async_set_unique_id(user_input["address"]) self._abort_if_unique_id_configured() + ubus = new_ubus_client(self.hass, user_input) + await ubus.api_list() # Check connection title = "%s - %s" % (user_input["id"], user_input["address"]) return self.async_create_entry(title=title, data=user_input) diff --git a/custom_components/openwrt/coordinator.py b/custom_components/openwrt/coordinator.py index cb914de..92cc94c 100644 --- a/custom_components/openwrt/coordinator.py +++ b/custom_components/openwrt/coordinator.py @@ -4,6 +4,7 @@ DataUpdateCoordinator, UpdateFailed, ) +from homeassistant.util.json import json_loads from .ubus import Ubus from .constants import DOMAIN @@ -180,6 +181,23 @@ async def do_file_exec(self, command: str, params, env: dict, extra: dict): **extra, }, ) + def process_output(data: str): + try: + json = json_loads(data) + if type(json) is list or type(json) is dict: + return json + except: + pass + return data.strip().split("\n") + return { + "code": result.get("code", 1), + "stdout": process_output(result.get("stdout", "")), + "stderr": process_output(result.get("stderr", "")), + } + + async def do_ubus_call(self, subsystem: str, method: str, params: dict): + _LOGGER.debug(f"do_ubus_call(): {subsystem} / {method}: {params}") + return await self._ubus.api_call(subsystem, method, params) async def do_rc_init(self, name: str, action: str): _LOGGER.debug( @@ -262,7 +280,7 @@ async def update_wan_info(self): return result async def load_ubus(self): - return await self._ubus.api_call("*", None, None, "list") + return await self._ubus.api_list() def is_api_supported(self, name: str) -> bool: if self._apis and name in self._apis: @@ -291,18 +309,21 @@ async def async_update_data(): raise UpdateFailed(f"OpenWrt communication error: {err}") return async_update_data - -def new_coordinator(hass, config: dict, all_devices: dict) -> DeviceCoordinator: - _LOGGER.debug(f"new_coordinator: {config}") +def new_ubus_client(hass, config: dict) -> Ubus: + _LOGGER.debug(f"new_ubus_client(): {config}") schema = "https" if config["https"] else "http" port = ":%d" % (config["port"]) if config["port"] > 0 else '' url = "%s://%s%s%s" % (schema, config["address"], port, config["path"]) - connection = Ubus( + return Ubus( hass.async_add_executor_job, url, config["username"], config.get("password", ""), verify=config.get("verify_cert", True) ) + +def new_coordinator(hass, config: dict, all_devices: dict) -> DeviceCoordinator: + _LOGGER.debug(f"new_coordinator: {config}, {all_devices}") + connection = new_ubus_client(hass, config) device = DeviceCoordinator(hass, config, connection, all_devices) return device \ No newline at end of file diff --git a/custom_components/openwrt/services.yaml b/custom_components/openwrt/services.yaml index 9f3ced5..a7436ed 100644 --- a/custom_components/openwrt/services.yaml +++ b/custom_components/openwrt/services.yaml @@ -16,6 +16,13 @@ exec: example: "wifi reload" selector: text: {} + arguments: + name: Command arguments + description: Arguments to append to the command (one per line) + required: false + selector: + text: + multiline: true environment: name: Environment variables description: Map of Environment variables names with values @@ -55,4 +62,30 @@ init: - "restart" - "reload" - "enable" - - "disable" \ No newline at end of file + - "disable" +ubus: + name: Make arbitrary Ubus call + target: + device: + integration: openwrt + fields: + subsystem: + name: Ubus sub-system + description: Top-level Ubus sub-system + required: true + example: "system" + selector: + text: {} + method: + name: Ubus method + description: Ubus method to call + required: true + example: "board" + selector: + text: {} + parameters: + name: Call parameters + description: Ubus call paramteres + required: false + selector: + object: {} diff --git a/custom_components/openwrt/ubus.py b/custom_components/openwrt/ubus.py index b2ff067..b52e3d1 100644 --- a/custom_components/openwrt/ubus.py +++ b/custom_components/openwrt/ubus.py @@ -119,3 +119,6 @@ def post(): if result_code == 0: return json_response['result'][1] if len(result) > 1 else {} raise ConnectionError(f"rpc error: {result[0]}") + + async def api_list(self): + return await self.api_call("*", None, None, "list") From b656fcfd846fb59865c9d5581a4668ce7ff7855e Mon Sep 17 00:00:00 2001 From: Konstantin Vorobyev Date: Mon, 22 Jan 2024 07:35:55 +0100 Subject: [PATCH 2/2] Update version field --- custom_components/openwrt/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/openwrt/manifest.json b/custom_components/openwrt/manifest.json index d918ff5..160eaf3 100644 --- a/custom_components/openwrt/manifest.json +++ b/custom_components/openwrt/manifest.json @@ -8,6 +8,6 @@ "requirements": [], "iot_class": "local_polling", "config_flow": true, - "version": "0.0.2" + "version": "0.1.0" }