From d9056235bef79e52ecc3bfe14812429ceb42d6b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:18:31 +0000 Subject: [PATCH 1/6] Add SSH port/passphrase support with migration Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- README.md | 6 ++- __init__.py | 30 ++++++++++++-- config_flow.py | 17 ++++++-- const.py | 4 ++ coordinator.py | 7 +++- options_flow.py | 16 +++++++- strings.json | 8 ++++ tests/unit_tests/test_config_flow.py | 51 ++++++++++++++++++++++- tests/unit_tests/test_init.py | 39 +++++++++++++++++- tests/unit_tests/test_options_flow.py | 58 ++++++++++++++++++++++++++- translations/de.json | 8 ++++ translations/en.json | 8 ++++ 12 files changed, 235 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 686d191..67229b6 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,11 @@ Fill in the following fields: | **Name** | ✅ | Friendly display name for this entry (must be unique across all entries) | | **Service** | ✅ | Name of the Docker container on the remote host | | **Host** | ✅ | Hostname or IP address of the remote server | +| **Port** | — | SSH port of the remote server (default: `22`) | | **Username** | ✅ | SSH username | | **Password** | ⚠️ | SSH password (use instead of `key_file`) | | **Key file** | ⚠️ | Path to an SSH private key file on the HA host (use instead of `password`) | +| **Passphrase** | — | Passphrase for `key_file` (default: empty) | | **Check known hosts** | — | Verify host key against known hosts (default: `true`) | | **Known hosts** | — | Path or string of the known hosts (only valid when `check_known_hosts` is `true`) | | **Docker command** | — | The Docker executable on the remote host, e.g. `docker`, `sudo docker`, `podman` (default: `docker`) | @@ -96,7 +98,7 @@ Fill in the following fields: > **Note:** Either `password` or `key_file` must be provided. -When a new entry is added the integration validates the SSH connection and verifies that the named container exists on the host before creating the entry. +When a new entry is added the integration validates the SSH connection and verifies that the named container exists on the host before creating the entry. `passphrase` is only sent to `ssh_command.execute` when a non-empty value is configured. ### ⚙️ Options @@ -253,7 +255,7 @@ This service is useful for one-off diagnostic commands, health checks, or config ## 🔍 Automatic Discovery -When a new entry is successfully added, SSH Docker automatically scans the host for additional Docker containers and offers to add unconfigured ones as new entries. The discovery form is pre-filled with all SSH and Docker settings from the original entry — including the `check_for_updates` and `auto_update` values — so the user only needs to confirm the container name. +When a new entry is successfully added, SSH Docker automatically scans the host for additional Docker containers and offers to add unconfigured ones as new entries. The discovery form is pre-filled with all SSH and Docker settings from the original entry — including `port`, `check_for_updates`, and `auto_update` — so the user only needs to confirm the container name. Discovered container names are automatically capitalized in the **Name** field (e.g., `grocy` → `Grocy`) while the **Service** field retains the original lowercase name. diff --git a/__init__.py b/__init__.py index 9db9b7f..dd45782 100644 --- a/__init__.py +++ b/__init__.py @@ -15,11 +15,11 @@ from homeassistant.helpers.typing import ConfigType from .const import ( - DOMAIN, CONF_KEY_FILE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, + DOMAIN, CONF_PORT, CONF_KEY_FILE, CONF_PASSPHRASE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, CONF_DOCKER_COMMAND, CONF_AUTO_UPDATE, CONF_CHECK_FOR_UPDATES, CONF_SERVICE, - SERVICE_CREATE, SERVICE_RESTART, SERVICE_STOP, SERVICE_REMOVE, SERVICE_REFRESH, - SERVICE_GET_LOGS, SERVICE_EXECUTE_COMMAND, - DEFAULT_DOCKER_COMMAND, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_TIMEOUT, + SERVICE_CREATE, SERVICE_RESTART, SERVICE_STOP, SERVICE_REMOVE, SERVICE_REFRESH, SERVICE_GET_LOGS, + SERVICE_EXECUTE_COMMAND, DEFAULT_DOCKER_COMMAND, DEFAULT_PORT, DEFAULT_PASSPHRASE, + DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_TIMEOUT, DEFAULT_AUTO_UPDATE, DEFAULT_CHECK_FOR_UPDATES, DOCKER_SERVICES_EXECUTABLE, ) @@ -220,6 +220,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old config entries to include SSH port and passphrase defaults.""" + if config_entry.version > 2: + return False + + if config_entry.version == 1: + new_options = dict(config_entry.options) + new_options.setdefault(CONF_PORT, DEFAULT_PORT) + new_options.setdefault(CONF_PASSPHRASE, DEFAULT_PASSPHRASE) + hass.config_entries.async_update_entry( + config_entry, + options=new_options, + version=2, + ) + _LOGGER.info("Migrated config entry %s from version 1 to 2", config_entry.entry_id) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" _LOGGER.debug("Unloading config entry for container %s", entry.data.get(CONF_NAME)) @@ -290,6 +309,7 @@ async def _discover_services(hass: HomeAssistant, entry: ConfigEntry) -> None: CONF_SERVICE: service_name, CONF_NAME: service_name[0].upper() + service_name[1:] if service_name else service_name, CONF_HOST: host, + CONF_PORT: options.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: options.get(CONF_USERNAME, ""), CONF_DOCKER_COMMAND: options.get(CONF_DOCKER_COMMAND, DEFAULT_DOCKER_COMMAND), CONF_CHECK_KNOWN_HOSTS: options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), @@ -300,6 +320,8 @@ async def _discover_services(hass: HomeAssistant, entry: ConfigEntry) -> None: discovery_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): discovery_data[CONF_KEY_FILE] = options[CONF_KEY_FILE] + if CONF_PASSPHRASE in options: + discovery_data[CONF_PASSPHRASE] = options.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE) if options.get(CONF_KNOWN_HOSTS): discovery_data[CONF_KNOWN_HOSTS] = options[CONF_KNOWN_HOSTS] diff --git a/config_flow.py b/config_flow.py index e1e7888..886c79f 100644 --- a/config_flow.py +++ b/config_flow.py @@ -14,9 +14,10 @@ from homeassistant.core import HomeAssistant, callback from .const import ( - DOMAIN, CONF_SERVICE, CONF_KEY_FILE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, + DOMAIN, CONF_SERVICE, CONF_PORT, CONF_KEY_FILE, CONF_PASSPHRASE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, CONF_DOCKER_COMMAND, CONF_AUTO_UPDATE, CONF_CHECK_FOR_UPDATES, - DEFAULT_DOCKER_COMMAND, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_AUTO_UPDATE, DEFAULT_CHECK_FOR_UPDATES, + DEFAULT_DOCKER_COMMAND, DEFAULT_PORT, DEFAULT_PASSPHRASE, + DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_AUTO_UPDATE, DEFAULT_CHECK_FOR_UPDATES, SSH_COMMAND_DOMAIN, SSH_COMMAND_SERVICE_EXECUTE, SSH_CONF_OUTPUT, SSH_CONF_EXIT_STATUS, DOCKER_SERVICES_EXECUTABLE, DEFAULT_TIMEOUT, @@ -30,9 +31,11 @@ vol.Required(CONF_NAME): str, vol.Required(CONF_SERVICE): str, vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str, + vol.Optional(CONF_PASSPHRASE, default=DEFAULT_PASSPHRASE): str, vol.Optional(CONF_CHECK_KNOWN_HOSTS, default=DEFAULT_CHECK_KNOWN_HOSTS): bool, vol.Optional(CONF_KNOWN_HOSTS): str, vol.Optional(CONF_DOCKER_COMMAND, default=DEFAULT_DOCKER_COMMAND): str, @@ -66,6 +69,7 @@ async def _check_service_exists( service_data: dict[str, Any] = { CONF_HOST: options.get(CONF_HOST, ""), + CONF_PORT: options.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: options.get(CONF_USERNAME, ""), "check_known_hosts": options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), "command": discover_cmd, @@ -75,6 +79,8 @@ async def _check_service_exists( service_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): service_data["key_file"] = options[CONF_KEY_FILE] + if options.get(CONF_PASSPHRASE): + service_data["passphrase"] = options[CONF_PASSPHRASE] if options.get(CONF_KNOWN_HOSTS): service_data["known_hosts"] = options[CONF_KNOWN_HOSTS] @@ -137,9 +143,14 @@ def _build_user_schema(defaults: dict[str, Any]) -> vol.Schema: vol.Required(CONF_NAME, default=defaults.get(CONF_NAME, "")): str, vol.Required(CONF_SERVICE, default=defaults.get(CONF_SERVICE, "")): str, vol.Required(CONF_HOST, default=defaults.get(CONF_HOST, "")): str, + vol.Optional(CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)): int, vol.Required(CONF_USERNAME, default=defaults.get(CONF_USERNAME, "")): str, vol.Optional(CONF_PASSWORD, default=defaults.get(CONF_PASSWORD, "")): str, vol.Optional(CONF_KEY_FILE, default=defaults.get(CONF_KEY_FILE, "")): str, + vol.Optional( + CONF_PASSPHRASE, + default=defaults.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE), + ): str, vol.Optional( CONF_CHECK_KNOWN_HOSTS, default=defaults.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), @@ -164,7 +175,7 @@ def _build_user_schema(defaults: dict[str, Any]) -> vol.Schema: class SshDockerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SSH Docker.""" - VERSION = 1 + VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/const.py b/const.py index 82a5693..6cdae86 100644 --- a/const.py +++ b/const.py @@ -6,7 +6,9 @@ CONF_SERVICE = "service" +CONF_PORT = "port" CONF_KEY_FILE = "key_file" +CONF_PASSPHRASE = "passphrase" CONF_CHECK_KNOWN_HOSTS = "check_known_hosts" CONF_KNOWN_HOSTS = "known_hosts" CONF_DOCKER_COMMAND = "docker_command" @@ -34,6 +36,8 @@ SERVICE_EXECUTE_COMMAND = "execute_command" DEFAULT_DOCKER_COMMAND = "docker" +DEFAULT_PORT = 22 +DEFAULT_PASSPHRASE = "" DEFAULT_CHECK_KNOWN_HOSTS = True DEFAULT_AUTO_UPDATE = False DEFAULT_CHECK_FOR_UPDATES = False diff --git a/coordinator.py b/coordinator.py index cb8c528..b0a12f9 100644 --- a/coordinator.py +++ b/coordinator.py @@ -29,12 +29,12 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from .const import ( - DOMAIN, CONF_SERVICE, CONF_KEY_FILE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, + DOMAIN, CONF_SERVICE, CONF_PORT, CONF_KEY_FILE, CONF_PASSPHRASE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, CONF_DOCKER_COMMAND, CONF_AUTO_UPDATE, CONF_CHECK_FOR_UPDATES, CONF_UPDATE_AVAILABLE, CONF_CREATED, CONF_IMAGE, SSH_COMMAND_DOMAIN, SSH_COMMAND_SERVICE_EXECUTE, SSH_CONF_OUTPUT, SSH_CONF_EXIT_STATUS, - DEFAULT_DOCKER_COMMAND, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_TIMEOUT, + DEFAULT_DOCKER_COMMAND, DEFAULT_PORT, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_TIMEOUT, DOCKER_CREATE_EXECUTABLE, DOCKER_CREATE_TIMEOUT, DOCKER_PULL_TIMEOUT, DOCKER_SERVICES_EXECUTABLE, get_ssh_semaphore, @@ -76,6 +76,7 @@ async def _ssh_run( ) service_data: dict[str, Any] = { CONF_HOST: options[CONF_HOST], + CONF_PORT: options.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: options[CONF_USERNAME], "check_known_hosts": options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), "command": command, @@ -85,6 +86,8 @@ async def _ssh_run( service_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): service_data["key_file"] = options[CONF_KEY_FILE] + if options.get(CONF_PASSPHRASE): + service_data["passphrase"] = options[CONF_PASSPHRASE] if options.get(CONF_KNOWN_HOSTS): service_data["known_hosts"] = options[CONF_KNOWN_HOSTS] diff --git a/options_flow.py b/options_flow.py index 7d60040..104b044 100644 --- a/options_flow.py +++ b/options_flow.py @@ -12,9 +12,9 @@ from homeassistant.exceptions import ServiceValidationError, HomeAssistantError from .const import ( - CONF_KEY_FILE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, + CONF_PORT, CONF_KEY_FILE, CONF_PASSPHRASE, CONF_CHECK_KNOWN_HOSTS, CONF_KNOWN_HOSTS, CONF_DOCKER_COMMAND, CONF_AUTO_UPDATE, CONF_CHECK_FOR_UPDATES, - DEFAULT_DOCKER_COMMAND, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_AUTO_UPDATE, + DEFAULT_DOCKER_COMMAND, DEFAULT_PORT, DEFAULT_PASSPHRASE, DEFAULT_CHECK_KNOWN_HOSTS, DEFAULT_AUTO_UPDATE, DEFAULT_CHECK_FOR_UPDATES, DEFAULT_TIMEOUT, SSH_COMMAND_DOMAIN, SSH_COMMAND_SERVICE_EXECUTE, SSH_CONF_EXIT_STATUS, ) @@ -40,6 +40,7 @@ async def validate_and_build_options( service_data: dict[str, Any] = { CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: user_input[CONF_USERNAME], "check_known_hosts": user_input.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), "command": f"{docker_cmd} ps -q", @@ -49,6 +50,8 @@ async def validate_and_build_options( service_data[CONF_PASSWORD] = user_input[CONF_PASSWORD] if has_key_file: service_data["key_file"] = user_input[CONF_KEY_FILE] + if user_input.get(CONF_PASSPHRASE): + service_data["passphrase"] = user_input[CONF_PASSPHRASE] if user_input.get(CONF_KNOWN_HOSTS): service_data["known_hosts"] = user_input[CONF_KNOWN_HOSTS] @@ -99,7 +102,9 @@ async def validate_and_build_options( _LOGGER.debug("SSH validation successful for host %s", user_input[CONF_HOST]) options: dict[str, Any] = { CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSPHRASE: user_input.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE), CONF_CHECK_KNOWN_HOSTS: user_input.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), CONF_DOCKER_COMMAND: docker_cmd, CONF_AUTO_UPDATE: user_input.get(CONF_AUTO_UPDATE, DEFAULT_AUTO_UPDATE), @@ -118,9 +123,11 @@ async def validate_and_build_options( STEP_OPTIONS_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str, + vol.Optional(CONF_PASSPHRASE, default=DEFAULT_PASSPHRASE): str, vol.Optional(CONF_CHECK_KNOWN_HOSTS, default=DEFAULT_CHECK_KNOWN_HOSTS): bool, vol.Optional(CONF_KNOWN_HOSTS): str, vol.Optional(CONF_DOCKER_COMMAND, default=DEFAULT_DOCKER_COMMAND): str, @@ -156,9 +163,14 @@ async def async_step_init( schema = vol.Schema( { vol.Required(CONF_HOST, default=current.get(CONF_HOST, "")): str, + vol.Optional(CONF_PORT, default=current.get(CONF_PORT, DEFAULT_PORT)): int, vol.Required(CONF_USERNAME, default=current.get(CONF_USERNAME, "")): str, vol.Optional(CONF_PASSWORD, default=current.get(CONF_PASSWORD, "")): str, vol.Optional(CONF_KEY_FILE, default=current.get(CONF_KEY_FILE, "")): str, + vol.Optional( + CONF_PASSPHRASE, + default=current.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE), + ): str, vol.Optional( CONF_CHECK_KNOWN_HOSTS, default=current.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), diff --git a/strings.json b/strings.json index 1323c20..c294aef 100644 --- a/strings.json +++ b/strings.json @@ -8,9 +8,11 @@ "name": "Name", "service": "Service", "host": "Host", + "port": "Port", "username": "Username", "password": "Password", "key_file": "Key File", + "passphrase": "Passphrase", "check_known_hosts": "Check Known Hosts", "known_hosts": "Known Hosts", "docker_command": "Docker Command", @@ -21,9 +23,11 @@ "name": "A friendly display name for this container in Home Assistant (e.g. My Web Server).", "service": "The exact container name on the remote host, as shown by `docker ps` (e.g. my_web_server).", "host": "IP address or hostname of the remote machine running Docker.", + "port": "SSH port of the remote host (default: 22).", "username": "SSH username used to connect to the remote host.", "password": "SSH password. Leave empty if authenticating with a key file.", "key_file": "Absolute path to the SSH private key file on the Home Assistant machine. Leave empty if authenticating with a password.", + "passphrase": "Passphrase for the SSH private key file. Leave empty if the key has no passphrase.", "check_known_hosts": "Verify the remote host against a known_hosts file before connecting.", "known_hosts": "Absolute path to the SSH known_hosts file on the Home Assistant machine.", "docker_command": "Docker executable to use on the remote host (e.g. `docker` or `sudo docker`).", @@ -54,9 +58,11 @@ "title": "Update Docker Container Options", "data": { "host": "Host", + "port": "Port", "username": "Username", "password": "Password", "key_file": "Key File", + "passphrase": "Passphrase", "check_known_hosts": "Check Known Hosts", "known_hosts": "Known Hosts", "docker_command": "Docker Command", @@ -65,9 +71,11 @@ }, "data_description": { "host": "IP address or hostname of the remote machine running Docker.", + "port": "SSH port of the remote host (default: 22).", "username": "SSH username used to connect to the remote host.", "password": "SSH password. Leave empty if authenticating with a key file.", "key_file": "Absolute path to the SSH private key file on the Home Assistant machine. Leave empty if authenticating with a password.", + "passphrase": "Passphrase for the SSH private key file. Leave empty if the key has no passphrase.", "check_known_hosts": "Verify the remote host against a known_hosts file before connecting.", "known_hosts": "Absolute path to the SSH known_hosts file on the Home Assistant machine.", "docker_command": "Docker executable to use on the remote host (e.g. `docker` or `sudo docker`).", diff --git a/tests/unit_tests/test_config_flow.py b/tests/unit_tests/test_config_flow.py index 69b8810..904baf6 100644 --- a/tests/unit_tests/test_config_flow.py +++ b/tests/unit_tests/test_config_flow.py @@ -12,7 +12,9 @@ sys.path.insert(0, absolute_plugin_path) from ssh_docker.config_flow import SshDockerConfigFlow, _check_service_exists # noqa: E402 -from ssh_docker.const import CONF_SERVICE, SSH_CONF_OUTPUT, SSH_CONF_EXIT_STATUS # noqa: E402 +from ssh_docker.const import ( # noqa: E402 + CONF_SERVICE, CONF_PORT, CONF_PASSPHRASE, DEFAULT_PORT, SSH_CONF_OUTPUT, SSH_CONF_EXIT_STATUS, +) from homeassistant.config_entries import AbortFlowException # noqa: E402 from homeassistant.const import CONF_NAME, CONF_SOURCE # noqa: E402 @@ -213,8 +215,10 @@ async def test_discovery_step_prefills_ssh_options(self): CONF_NAME: "discovered_container", CONF_SERVICE: "discovered_container", "host": "192.168.1.100", + "port": 2222, "username": "admin", "password": "secret", + "passphrase": "key-passphrase", "docker_command": "sudo docker", } @@ -226,8 +230,10 @@ async def test_discovery_step_prefills_ssh_options(self): # The stored discovery info should have the service name and all SSH options self.assertEqual(flow._discovery_info[CONF_SERVICE], "discovered_container") self.assertEqual(flow._discovery_info["host"], "192.168.1.100") + self.assertEqual(flow._discovery_info["port"], 2222) self.assertEqual(flow._discovery_info["username"], "admin") self.assertEqual(flow._discovery_info["password"], "secret") + self.assertEqual(flow._discovery_info["passphrase"], "key-passphrase") self.assertEqual(flow._discovery_info["docker_command"], "sudo docker") async def test_discovery_step_aborts_when_already_configured(self): @@ -258,8 +264,10 @@ def _make_hass(self, ssh_response: dict) -> MagicMock: def _base_options(self) -> dict: return { "host": "192.168.1.100", + "port": DEFAULT_PORT, "username": "user", "password": "pass", + "passphrase": "", "check_known_hosts": True, "docker_command": "docker", } @@ -317,6 +325,47 @@ async def test_returns_none_when_output_empty(self): result = await _check_service_exists(hass, self._base_options(), "container_a") self.assertIsNone(result) + async def test_forwards_port_to_ssh_command(self): + """Configured SSH port is forwarded to ssh_command.execute.""" + hass = self._make_hass( + {SSH_CONF_OUTPUT: '["container_a"]', SSH_CONF_EXIT_STATUS: 0} + ) + options = self._base_options() + options[CONF_PORT] = 2200 + + await _check_service_exists(hass, options, "container_a") + + service_data = hass.services.async_call.call_args.args[2] + self.assertEqual(service_data[CONF_PORT], 2200) + + async def test_omits_empty_passphrase_from_ssh_command(self): + """Empty passphrase is not forwarded to ssh_command.execute.""" + hass = self._make_hass( + {SSH_CONF_OUTPUT: '["container_a"]', SSH_CONF_EXIT_STATUS: 0} + ) + options = self._base_options() + options["key_file"] = "/config/id_rsa" + options[CONF_PASSPHRASE] = "" + + await _check_service_exists(hass, options, "container_a") + + service_data = hass.services.async_call.call_args.args[2] + self.assertNotIn("passphrase", service_data) + + async def test_forwards_non_empty_passphrase_to_ssh_command(self): + """Non-empty passphrase is forwarded to ssh_command.execute.""" + hass = self._make_hass( + {SSH_CONF_OUTPUT: '["container_a"]', SSH_CONF_EXIT_STATUS: 0} + ) + options = self._base_options() + options["key_file"] = "/config/id_rsa" + options[CONF_PASSPHRASE] = "secret" + + await _check_service_exists(hass, options, "container_a") + + service_data = hass.services.async_call.call_args.args[2] + self.assertEqual(service_data["passphrase"], "secret") + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/test_init.py b/tests/unit_tests/test_init.py index 1ccc348..41be5a5 100644 --- a/tests/unit_tests/test_init.py +++ b/tests/unit_tests/test_init.py @@ -12,10 +12,10 @@ absolute_plugin_path = str(Path(__file__).parent.parent.parent.parent.absolute()) sys.path.insert(0, absolute_plugin_path) -from ssh_docker import async_setup, async_setup_entry # noqa: E402 +from ssh_docker import async_setup, async_setup_entry, async_migrate_entry # noqa: E402 from ssh_docker.const import ( # noqa: E402 DOMAIN, SERVICE_CREATE, SERVICE_RESTART, SERVICE_STOP, SERVICE_REMOVE, SERVICE_REFRESH, - SERVICE_GET_LOGS, SERVICE_EXECUTE_COMMAND, DEFAULT_TIMEOUT, + SERVICE_GET_LOGS, SERVICE_EXECUTE_COMMAND, DEFAULT_TIMEOUT, DEFAULT_PORT, DEFAULT_PASSPHRASE, ) from ssh_docker.frontend import SshDockerPanelRegistration # noqa: E402 from homeassistant.config_entries import ConfigEntry # noqa: E402 @@ -137,8 +137,10 @@ def _make_entry(service="my_container", host="192.168.1.100"): data={"name": service, "service": service}, options={ "host": host, + "port": DEFAULT_PORT, "username": "user", "password": "pass", + "passphrase": DEFAULT_PASSPHRASE, "docker_command": "docker", "check_known_hosts": True, }, @@ -423,5 +425,38 @@ async def mock_ssh_run(h, opts, cmd, timeout=DEFAULT_TIMEOUT): self.assertEqual(captured_timeout, 120) +class TestAsyncMigrateEntry(unittest.IsolatedAsyncioTestCase): + """Tests for async_migrate_entry.""" + + async def test_migrates_v1_entry_with_default_port_and_passphrase(self): + """Version 1 entries get default port and empty passphrase.""" + hass = MagicMock() + hass.config_entries.async_update_entry = MagicMock() + entry = MagicMock() + entry.entry_id = "entry_id" + entry.version = 1 + entry.options = {"host": "192.168.1.100", "username": "user", "password": "pass"} + + result = await async_migrate_entry(hass, entry) + self.assertTrue(result) + hass.config_entries.async_update_entry.assert_called_once() + kwargs = hass.config_entries.async_update_entry.call_args.kwargs + self.assertEqual(kwargs["version"], 2) + self.assertEqual(kwargs["options"]["port"], DEFAULT_PORT) + self.assertEqual(kwargs["options"]["passphrase"], DEFAULT_PASSPHRASE) + + async def test_returns_false_for_newer_version(self): + """Entries newer than supported migration version return False.""" + hass = MagicMock() + hass.config_entries.async_update_entry = MagicMock() + entry = MagicMock() + entry.entry_id = "entry_id" + entry.version = 3 + entry.options = {} + result = await async_migrate_entry(hass, entry) + self.assertFalse(result) + hass.config_entries.async_update_entry.assert_not_called() + + if __name__ == "__main__": unittest.main() diff --git a/tests/unit_tests/test_options_flow.py b/tests/unit_tests/test_options_flow.py index 8301d2b..ab03e7d 100644 --- a/tests/unit_tests/test_options_flow.py +++ b/tests/unit_tests/test_options_flow.py @@ -11,7 +11,8 @@ absolute_plugin_path = str(Path(__file__).parent.parent.parent.parent.absolute()) sys.path.insert(0, absolute_plugin_path) -from ssh_docker.options_flow import SshDockerOptionsFlow # noqa: E402 +from ssh_docker.options_flow import SshDockerOptionsFlow, validate_and_build_options # noqa: E402 +from ssh_docker.const import DEFAULT_PORT, DEFAULT_PASSPHRASE # noqa: E402 from homeassistant.config_entries import ConfigEntry # noqa: E402 @@ -27,8 +28,10 @@ def _make_flow(self, current_options=None): data={"name": "my_container"}, options=current_options or { "host": "192.168.1.100", + "port": DEFAULT_PORT, "username": "user", "password": "pass", + "passphrase": DEFAULT_PASSPHRASE, "docker_command": "docker", "check_known_hosts": True, "auto_update": False, @@ -50,16 +53,20 @@ async def test_init_step_updates_options_on_valid_input(self): flow = self._make_flow() user_input = { "host": "192.168.1.200", + "port": 2200, "username": "newuser", "password": "newpass", + "passphrase": "", "docker_command": "docker", "check_known_hosts": True, "auto_update": True, } new_options = { "host": "192.168.1.200", + "port": 2200, "username": "newuser", "password": "newpass", + "passphrase": "", "docker_command": "docker", "check_known_hosts": True, "auto_update": True, @@ -93,5 +100,54 @@ async def test_init_step_shows_error_on_validation_failure(self): self.assertEqual(result["errors"]["base"], "password_or_key_file_required") +class TestValidateAndBuildOptions(unittest.IsolatedAsyncioTestCase): + """Tests for validate_and_build_options service payload behavior.""" + + def _make_hass(self): + hass = MagicMock() + hass.services.async_call = AsyncMock(return_value={"exit_status": 0}) + return hass + + def _base_input(self): + return { + "host": "192.168.1.200", + "port": 2200, + "username": "newuser", + "key_file": "/config/id_rsa", + "passphrase": "", + "docker_command": "docker", + "check_known_hosts": True, + "auto_update": False, + "check_for_updates": False, + } + + async def test_omits_empty_passphrase_for_ssh_execute(self): + """Empty passphrase is not sent to ssh_command.execute.""" + hass = self._make_hass() + user_input = self._base_input() + + options, error_key = await validate_and_build_options(hass, user_input) + + self.assertIsNone(error_key) + self.assertEqual(options["passphrase"], "") + service_data = hass.services.async_call.call_args.args[2] + self.assertNotIn("passphrase", service_data) + self.assertEqual(service_data["port"], 2200) + + async def test_includes_non_empty_passphrase_for_ssh_execute(self): + """Non-empty passphrase is sent to ssh_command.execute.""" + hass = self._make_hass() + user_input = self._base_input() + user_input["passphrase"] = "secret" + + options, error_key = await validate_and_build_options(hass, user_input) + + self.assertIsNone(error_key) + self.assertEqual(options["passphrase"], "secret") + service_data = hass.services.async_call.call_args.args[2] + self.assertEqual(service_data["passphrase"], "secret") + self.assertEqual(service_data["port"], 2200) + + if __name__ == "__main__": unittest.main() diff --git a/translations/de.json b/translations/de.json index 44f86a9..9667dbf 100644 --- a/translations/de.json +++ b/translations/de.json @@ -8,9 +8,11 @@ "name": "Name", "service": "Dienst", "host": "Host", + "port": "Port", "username": "Benutzername", "password": "Passwort", "key_file": "Schlüsseldatei", + "passphrase": "Passphrase", "check_known_hosts": "Bekannte Hosts prüfen", "known_hosts": "Bekannte Hosts", "docker_command": "Docker-Befehl", @@ -21,9 +23,11 @@ "name": "Ein freundlicher Anzeigename für diesen Container in Home Assistant (z. B. Mein Webserver).", "service": "Der genaue Containername auf dem entfernten Host, wie er durch `docker ps` angezeigt wird (z. B. mein_webserver).", "host": "IP-Adresse oder Hostname des entfernten Rechners, auf dem Docker läuft.", + "port": "SSH-Port des entfernten Hosts (Standard: 22).", "username": "SSH-Benutzername für die Verbindung zum entfernten Host.", "password": "SSH-Passwort. Leer lassen, wenn die Authentifizierung mit einer Schlüsseldatei erfolgt.", "key_file": "Absoluter Pfad zur privaten SSH-Schlüsseldatei auf dem Home-Assistant-Rechner. Leer lassen, wenn die Authentifizierung mit einem Passwort erfolgt.", + "passphrase": "Passphrase für die private SSH-Schlüsseldatei. Leer lassen, wenn der Schlüssel keine Passphrase hat.", "check_known_hosts": "Den entfernten Host vor dem Verbinden gegen eine known_hosts-Datei prüfen.", "known_hosts": "Absoluter Pfad zur SSH-known_hosts-Datei auf dem Home-Assistant-Rechner.", "docker_command": "Auf dem entfernten Host zu verwendendes Docker-Programm (z. B. `docker` oder `sudo docker`).", @@ -54,9 +58,11 @@ "title": "Docker-Container-Optionen aktualisieren", "data": { "host": "Host", + "port": "Port", "username": "Benutzername", "password": "Passwort", "key_file": "Schlüsseldatei", + "passphrase": "Passphrase", "check_known_hosts": "Bekannte Hosts prüfen", "known_hosts": "Bekannte Hosts", "docker_command": "Docker-Befehl", @@ -65,9 +71,11 @@ }, "data_description": { "host": "IP-Adresse oder Hostname des entfernten Rechners, auf dem Docker läuft.", + "port": "SSH-Port des entfernten Hosts (Standard: 22).", "username": "SSH-Benutzername für die Verbindung zum entfernten Host.", "password": "SSH-Passwort. Leer lassen, wenn die Authentifizierung mit einer Schlüsseldatei erfolgt.", "key_file": "Absoluter Pfad zur privaten SSH-Schlüsseldatei auf dem Home-Assistant-Rechner. Leer lassen, wenn die Authentifizierung mit einem Passwort erfolgt.", + "passphrase": "Passphrase für die private SSH-Schlüsseldatei. Leer lassen, wenn der Schlüssel keine Passphrase hat.", "check_known_hosts": "Den entfernten Host vor dem Verbinden gegen eine known_hosts-Datei prüfen.", "known_hosts": "Absoluter Pfad zur SSH-known_hosts-Datei auf dem Home-Assistant-Rechner.", "docker_command": "Auf dem entfernten Host zu verwendendes Docker-Programm (z. B. `docker` oder `sudo docker`).", diff --git a/translations/en.json b/translations/en.json index 1323c20..c294aef 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,9 +8,11 @@ "name": "Name", "service": "Service", "host": "Host", + "port": "Port", "username": "Username", "password": "Password", "key_file": "Key File", + "passphrase": "Passphrase", "check_known_hosts": "Check Known Hosts", "known_hosts": "Known Hosts", "docker_command": "Docker Command", @@ -21,9 +23,11 @@ "name": "A friendly display name for this container in Home Assistant (e.g. My Web Server).", "service": "The exact container name on the remote host, as shown by `docker ps` (e.g. my_web_server).", "host": "IP address or hostname of the remote machine running Docker.", + "port": "SSH port of the remote host (default: 22).", "username": "SSH username used to connect to the remote host.", "password": "SSH password. Leave empty if authenticating with a key file.", "key_file": "Absolute path to the SSH private key file on the Home Assistant machine. Leave empty if authenticating with a password.", + "passphrase": "Passphrase for the SSH private key file. Leave empty if the key has no passphrase.", "check_known_hosts": "Verify the remote host against a known_hosts file before connecting.", "known_hosts": "Absolute path to the SSH known_hosts file on the Home Assistant machine.", "docker_command": "Docker executable to use on the remote host (e.g. `docker` or `sudo docker`).", @@ -54,9 +58,11 @@ "title": "Update Docker Container Options", "data": { "host": "Host", + "port": "Port", "username": "Username", "password": "Password", "key_file": "Key File", + "passphrase": "Passphrase", "check_known_hosts": "Check Known Hosts", "known_hosts": "Known Hosts", "docker_command": "Docker Command", @@ -65,9 +71,11 @@ }, "data_description": { "host": "IP address or hostname of the remote machine running Docker.", + "port": "SSH port of the remote host (default: 22).", "username": "SSH username used to connect to the remote host.", "password": "SSH password. Leave empty if authenticating with a key file.", "key_file": "Absolute path to the SSH private key file on the Home Assistant machine. Leave empty if authenticating with a password.", + "passphrase": "Passphrase for the SSH private key file. Leave empty if the key has no passphrase.", "check_known_hosts": "Verify the remote host against a known_hosts file before connecting.", "known_hosts": "Absolute path to the SSH known_hosts file on the Home Assistant machine.", "docker_command": "Docker executable to use on the remote host (e.g. `docker` or `sudo docker`).", From 1b8902775b4b10c2f4374212cfedb06df29051bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:19:02 +0000 Subject: [PATCH 2/6] Propagate passphrase default in discovery prefill Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- __init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index dd45782..4a223f4 100644 --- a/__init__.py +++ b/__init__.py @@ -320,8 +320,7 @@ async def _discover_services(hass: HomeAssistant, entry: ConfigEntry) -> None: discovery_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): discovery_data[CONF_KEY_FILE] = options[CONF_KEY_FILE] - if CONF_PASSPHRASE in options: - discovery_data[CONF_PASSPHRASE] = options.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE) + discovery_data[CONF_PASSPHRASE] = options.get(CONF_PASSPHRASE, DEFAULT_PASSPHRASE) if options.get(CONF_KNOWN_HOSTS): discovery_data[CONF_KNOWN_HOSTS] = options[CONF_KNOWN_HOSTS] From 862c76e85fbc08afcf47a1fbbdb0f949449ce9b3 Mon Sep 17 00:00:00 2001 From: gensyn <36128035+gensyn@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:36:51 +0200 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- options_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/options_flow.py b/options_flow.py index 104b044..816e100 100644 --- a/options_flow.py +++ b/options_flow.py @@ -121,9 +121,8 @@ async def validate_and_build_options( STEP_OPTIONS_DATA_SCHEMA = vol.Schema( - { vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str, From b7cc7c9e2df9528305317108603101e7b142ea02 Mon Sep 17 00:00:00 2001 From: gensyn <36128035+gensyn@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:37:04 +0200 Subject: [PATCH 4/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_flow.py b/config_flow.py index 886c79f..d86450e 100644 --- a/config_flow.py +++ b/config_flow.py @@ -31,7 +31,7 @@ vol.Required(CONF_NAME): str, vol.Required(CONF_SERVICE): str, vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str, From 33933c9618498ef1c43c7ad4d40b226b0e6e6e05 Mon Sep 17 00:00:00 2001 From: gensyn Date: Tue, 1 Sep 2026 11:42:08 +0200 Subject: [PATCH 5/6] Fixed passphrase being sent when no key file was selected --- config_flow.py | 16 ++++++++-------- coordinator.py | 16 ++++++++-------- options_flow.py | 16 ++++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/config_flow.py b/config_flow.py index 886c79f..3bc4909 100644 --- a/config_flow.py +++ b/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_COMMAND, CONF_TIMEOUT from homeassistant.core import HomeAssistant, callback from .const import ( @@ -71,18 +71,18 @@ async def _check_service_exists( CONF_HOST: options.get(CONF_HOST, ""), CONF_PORT: options.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: options.get(CONF_USERNAME, ""), - "check_known_hosts": options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), - "command": discover_cmd, - "timeout": DEFAULT_TIMEOUT, + CONF_CHECK_KNOWN_HOSTS: options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), + CONF_COMMAND: discover_cmd, + CONF_TIMEOUT: DEFAULT_TIMEOUT, } if options.get(CONF_PASSWORD): service_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): - service_data["key_file"] = options[CONF_KEY_FILE] - if options.get(CONF_PASSPHRASE): - service_data["passphrase"] = options[CONF_PASSPHRASE] + service_data[CONF_KEY_FILE] = options[CONF_KEY_FILE] + if options.get(CONF_PASSPHRASE): + service_data[CONF_PASSPHRASE] = options[CONF_PASSPHRASE] if options.get(CONF_KNOWN_HOSTS): - service_data["known_hosts"] = options[CONF_KNOWN_HOSTS] + service_data[CONF_KNOWN_HOSTS] = options[CONF_KNOWN_HOSTS] try: response = await hass.services.async_call( diff --git a/coordinator.py b/coordinator.py index b0a12f9..e0ab169 100644 --- a/coordinator.py +++ b/coordinator.py @@ -24,7 +24,7 @@ from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_COMMAND, CONF_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -78,18 +78,18 @@ async def _ssh_run( CONF_HOST: options[CONF_HOST], CONF_PORT: options.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: options[CONF_USERNAME], - "check_known_hosts": options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), - "command": command, - "timeout": timeout, + CONF_CHECK_KNOWN_HOSTS: options.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), + CONF_COMMAND: command, + CONF_TIMEOUT: timeout, } if options.get(CONF_PASSWORD): service_data[CONF_PASSWORD] = options[CONF_PASSWORD] if options.get(CONF_KEY_FILE): - service_data["key_file"] = options[CONF_KEY_FILE] - if options.get(CONF_PASSPHRASE): - service_data["passphrase"] = options[CONF_PASSPHRASE] + service_data[CONF_KEY_FILE] = options[CONF_KEY_FILE] + if options.get(CONF_PASSPHRASE): + service_data[CONF_PASSPHRASE] = options[CONF_PASSPHRASE] if options.get(CONF_KNOWN_HOSTS): - service_data["known_hosts"] = options[CONF_KNOWN_HOSTS] + service_data[CONF_KNOWN_HOSTS] = options[CONF_KNOWN_HOSTS] async def _call() -> Any: return await hass.services.async_call( diff --git a/options_flow.py b/options_flow.py index 104b044..9df0884 100644 --- a/options_flow.py +++ b/options_flow.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import OptionsFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_COMMAND, CONF_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError, HomeAssistantError @@ -42,18 +42,18 @@ async def validate_and_build_options( CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), CONF_USERNAME: user_input[CONF_USERNAME], - "check_known_hosts": user_input.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), - "command": f"{docker_cmd} ps -q", - "timeout": DEFAULT_TIMEOUT, + CONF_CHECK_KNOWN_HOSTS: user_input.get(CONF_CHECK_KNOWN_HOSTS, DEFAULT_CHECK_KNOWN_HOSTS), + CONF_COMMAND: f"{docker_cmd} ps -q", + CONF_TIMEOUT: DEFAULT_TIMEOUT, } if has_password: service_data[CONF_PASSWORD] = user_input[CONF_PASSWORD] if has_key_file: - service_data["key_file"] = user_input[CONF_KEY_FILE] - if user_input.get(CONF_PASSPHRASE): - service_data["passphrase"] = user_input[CONF_PASSPHRASE] + service_data[CONF_KEY_FILE] = user_input[CONF_KEY_FILE] + if user_input.get(CONF_PASSPHRASE): + service_data[CONF_PASSPHRASE] = user_input[CONF_PASSPHRASE] if user_input.get(CONF_KNOWN_HOSTS): - service_data["known_hosts"] = user_input[CONF_KNOWN_HOSTS] + service_data[CONF_KNOWN_HOSTS] = user_input[CONF_KNOWN_HOSTS] _LOGGER.debug( "Validating SSH connection to %s as %s", From a771f1c12e1f1cf72ac41846e94220426c3d9e01 Mon Sep 17 00:00:00 2001 From: gensyn Date: Tue, 1 Sep 2026 11:48:00 +0200 Subject: [PATCH 6/6] Fixed passphrase being sent when no key file was selected --- config_flow.py | 2 +- options_flow.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config_flow.py b/config_flow.py index 1303ca8..d183ba5 100644 --- a/config_flow.py +++ b/config_flow.py @@ -31,7 +31,7 @@ vol.Required(CONF_NAME): str, vol.Required(CONF_SERVICE): str, vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(int, vol.Range(min=1, max=65535)), vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str, diff --git a/options_flow.py b/options_flow.py index 97b9637..7e65541 100644 --- a/options_flow.py +++ b/options_flow.py @@ -120,9 +120,9 @@ async def validate_and_build_options( return options, None -STEP_OPTIONS_DATA_SCHEMA = vol.Schema( +STEP_OPTIONS_DATA_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.All(int, vol.Range(min=1, max=65535)), vol.Required(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Optional(CONF_KEY_FILE): str,