How to add new user with command line? or even to change user password with command line?

I want to add to the system the option for end-user (not admin) to create new user in a friendly way

1 Like

Hi @joelgamal, did you find a way to add the user “programmatically” via command line?

I am trying to automate the provisioning process, but I am stuck at the user account creating.

Not yet, unfortunately

@joelgamal thanks for your reply!

that’s really too bad and disappointing! This means it is really tedious to automate the installation process without user interactions or having to deal with all those files inside “/usr/share/hassio/homeassistant/.storage”.

2 Likes

I would like to know this too. I need to setup HA inside docker container for some automated tests and I do not want to access ui each time I start from zero. For me I would appreciate a way to start HA with command to create a defined user and defined Long Live Access Token.

Has someone found a solution for this? I am trying to automate the installation process so I am able to restore everything from configurations, but I couldn’t find a away to create an user.

Also want same functionality.

It occurs to me idea here is using Selenium. I know that is dirty hack, but until this feature will not be implemented I think it is viable solution.

Steps here simple:

  1. Up home assistant in docker locally
  2. Add admin user via Selenium automation (same for long live tokens)
  3. Move config to server or test what you want

There is feature request for topic? Anyone found?

Seems I found solution. Just little reverse engineered onboarding process with browser developer tools. Hope this helps for someone.

For create user you could do:

curl --location --request POST 'http://192.168.1.1:8123/api/onboarding/users' \
--header 'Content-Type: application/json' \
--data-raw '{
    "client_id": "http://192.168.1.1:8123/",
    "name": "admin",
    "username": "admin",
    "password": "12345",
    "language": "en"
}'

or same on python:

import requests
import json

url = "http://192.168.1.42:8124/api/onboarding/users"

payload = json.dumps({
  "client_id": "http://192.168.1.42:8123/",
  "name": "admin",
  "username": "admin",
  "password": "12345",
  "language": "en"
})
headers = {
  'Content-Type': 'application/json'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)

IP in client_id match to exposed IP. But I don’t test another options.

Next step for me is trying to create long live tokens via curl.

2 Likes

I couldn’t get this to work. Is it still working for you? Can this be done using the CLI?

@jpoeppelman1, @kwill If you or anyone else is trying to do this in a docker container, here’s the script I wrote to to do it (it takes env vars for input so it can be used in containerized jobs):

I’m actually running home assistant using a helm chart I wrote for Kubernetes, and I’m using the above script in a post install helm hook job to complete the onboarding, so the registration is disabled quickly.

NOTE: To completely disable registration and not show any errors, you still need to mount a /config/configuration.yaml file with at least ALL of the basic info listed here:

homeassistant:
  name: Home
  latitude: 32.87336
  longitude: 117.22743
  elevation: 430
  unit_system: metric
  currency: USD
  country: US
  time_zone: "America/Los_Angeles"
  external_url: "https://www.example.com"

See more info on basic config details in the home assistant docs under configuration/basics. Hope this helps :pray:

1 Like

I had a similar problem, I wanted to do the initial onboarding automatically when I deployed a new container.

Tested on Core 2024.11.3, Frontend 20241106.2. Home Assistant Image: sha256:bfdad8504077fc6076cb746100ae20a9a39b9c49bceb48ea78c65e9013107dd5

import requests
import sys
import time
import argparse

# Timeout interval in seconds between polling attempts
polling_interval = 5

# HTTP headers needed for multiple requests
headers = {
    "Content-Type": "application/json",
}

def poll_until_ready(base_url, timeout=60):
    start_time = time.time()
    while True:
        try:
            # Send a GET request to the base URL
            response = requests.get(base_url)
            # Check if the status code is 200
            if response.status_code == 200:
                print(f"Server is ready! Received 200 status code from {base_url}")
                return True
            else:
                print(f"Received status code {response.status_code}. Retrying...")
        except requests.ConnectionError:
            print(f"Unable to connect to {base_url}. Retrying...")
        except Exception as e:
            print(f"Unexpected error: {str(e)}. Retrying...")

        # Break the loop if timeout is reached
        elapsed_time = time.time() - start_time
        if elapsed_time > timeout:
            print(f"Timeout reached after {timeout} seconds. Server not ready.")
            return False

        # Wait for the specified polling interval before retrying
        time.sleep(polling_interval)

def create_user(user_data):
    print("Starting Step 1: Creating user account...")
    try:
        url = f"{base_url}/api/onboarding/users"
        response = requests.post(url, json=user_data, headers=headers)
        response.raise_for_status()
        data = response.json()
        if "auth_code" in data:
            return data["auth_code"]
        else:
            print("Failed to retrieve auth_code.")
    except requests.RequestException as e:
        print(f"Failed to create user: {e}")

def exchange_auth_code(auth_code, user_data):
    print("Starting Step 2: Exchanging auth_code for tokens...")
    try:
        url = f"{base_url}/auth/token"
        payload = {
            "client_id": user_data["client_id"],
            "grant_type": "authorization_code",
            "code": auth_code,
        }
        headers = {
            "Content-Type": "application/x-www-form-urlencoded",
        }
        response = requests.post(url, data=payload, headers=headers)
        response.raise_for_status()
        data = response.json()
        if "access_token" in data:
            return data["access_token"]
        else:
            print("Failed to retrieve access token.")
    except requests.RequestException as e:
        print(f"Failed to exchange auth_code: {e}")

def complete_core_config(access_token):
    print("Starting Step 3: Completing core configuration...")
    try:
        url = f"{base_url}/api/onboarding/core_config"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        }
        response = requests.post(url, json={}, headers=headers)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"Failed to complete core configuration: {e}")

def complete_analytics(access_token):
    print("Starting Step 4: Completing analytics setup...")
    try:
        url = f"{base_url}/api/onboarding/analytics"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        }
        response = requests.post(url, json={}, headers=headers)
        response.raise_for_status()
    except requests.RequestException as e:
        print(f"Failed to complete analytics setup: {e}")

def complete_integration(access_token):
    print("Starting Step 5: Completing integration setup...")

    integration_data = {
        "client_id": f"{base_url}",
        "redirect_uri": f"{base_url}/auth/external/callback",
    }

    try:
        url = f"{base_url}/api/onboarding/integration"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        }
        response = requests.post(url, json=integration_data, headers=headers)
        response.raise_for_status()
    except requests.RequestException as e:
        log_error(f"Failed to complete integration step: {e}")

## Not needed but good for debugging
# def check_onboarding_status(access_token=None):
#         print("Checking onboarding status...")
#         try:
#             url = f"{base_url}/api/onboarding"
#             headers = {"Authorization": f"Bearer {access_token}"} if access_token else {}
#             response = requests.get(url, headers=headers)
#             response.raise_for_status()
#             data = response.json()
#             print(f"Response: {data}")
#             return data
#         except requests.RequestException as e:
#             print(f"Failed to retrieve onboarding status: {e}")

def main(user_data, base_url, username, password, language, timeout):
    poll_until_ready(base_url, timeout=timeout)

    try:
        auth_code = create_user(user_data)
        print("User account created successfully.")
    except Exception as e:
        print(f"Failed to create user: {e}")
        return

    try:
        access_token = exchange_auth_code(auth_code, user_data)
        print("Access token retrieved successfully.")
    except Exception as e:
        print(f"Failed to exchange auth code: {e}")
        return

    try:
        complete_core_config(access_token)
        print("Core configuration completed successfully.")
    except Exception as e:
        print(f"Failed to complete core configuration: {e}")
        return

    try:
        complete_analytics(access_token)
        print("Analytics enabled successfully.")
    except Exception as e:
        print(f"Failed to enable analytics: {e}")
        return

    try:
        complete_integration(access_token)
        print("Integration completed successfully.")
    except Exception as e:
        print(f"Failed to complete integration: {e}")
        return

    print("Onboarding process completed successfully.")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Completes the initial onboarding process for Home Assistant.")
    parser.add_argument(
        "--base-url",
        required=True,
        help="Base URL of the Home Assistant instance to poll with http/s. Example: http://localhost:8123",
    )
    parser.add_argument(
        "--username",
        required=True,
        help="Username for the user account to create.",
    )
    parser.add_argument(
        "--password",
        required=True,
        help="Password for the user account to create.",
    )
    parser.add_argument(
        "--language",
        default="en",
        help="Language for the user account (default: en).",
    )
    parser.add_argument(
        "--timeout",
        type=int,
        default=60,
        help="Timeout in seconds to wait for the server to return a 200 status code (default: 60).",
    )

    args = parser.parse_args()

    # Extract values from the parsed arguments
    base_url = args.base_url
    timeout = args.timeout
    username = args.username
    password = args.password
    language = args.language

    # User credentials for onboarding
    user_data = {
        "client_id": f"{base_url}",
        "name": f"{username}",        # Users display name
        "username": f"{username}",    # Users login name
        "password": f"{password}",
        "language": f"{language}",
    }

    main(user_data, base_url, username, password, language, timeout)

I then run this script on my server, placed by Ansible and creates a marker file so it only runs once. I’m sure you could add a successful marker file placement in the script, and not run if it exists, but I use this pattern elsewhere in my setup so I’d like to keep it consistent.

As mentioned above, you still need to populate the values like name and latitude in the homeassistant.yaml config file. What I do is drop a docker-compose file onto a server, and the homeassistant.yaml config file into the location docker will mount its config, then run the onboarding script and restart the container.

# Run the initial onboarding
- name: Copy the Python script to the target machine
  copy:
    src: ./scripts/homeassistant_onboarding/onboarding.py
    dest: /tmp/onboarding.py
    mode: '0755'

- name: Run the Python script
  command: python3 /tmp/onboarding.py --base-url=http://localhost --username={{ homeassistant_admin_username }} --password={{ homeassistant_admin_password }}
  args:
    creates: /var/lib/onboarding_success.marker
  register: python_script_result
  notify: Restart Home Assistant

# Stops this getting run more than once successfully
- name: Create a marker file upon successful execution
  file:
    path: /var/lib/onboarding_success.marker
    state: touch
  when: python_script_result is succeeded

# - name: Display the output of the Python script
#   debug:
#     var: python_script_result.stdout

This is a very niche thing to want to do, but this thread helped me, so I’m giving the result back to the community.

Expect the internal APIs to change at any time, breaking this script.