Compare commits

...
8 Commits
Author SHA1 Message Date
renovate[bot] 39cd7ea918 Update TeslaMate to v4.2.0 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-23 14:12:16 +01:00
renovate[bot]andColin Seymour ff39e95482 Update TeslaMate to v4.1.1 (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Colin Seymour <colin@symr.io>
2026-08-19 17:21:24 +01:00
renovate[bot]andColin Seymour 8b048671cb Update frenck/action-addon-linter action to v2.21.1 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Colin Seymour <colin@symr.io>
2026-08-19 17:18:41 +01:00
Colin Seymour 932352d43a Update integration tests and update dev environment (#152) 2026-08-19 17:14:15 +01:00
renovate[bot] b21b77c5b0 Update actions/checkout action to v7.0.1 (#149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-21 06:35:27 +01:00
renovate[bot] 0de3dd5aa2 Update softprops/action-gh-release action to v3.0.2 (#148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-13 20:08:33 +01:00
Colin Seymour 6b07490526 Update and improve dev env, testing, building and publishing (#143) 2026-06-21 13:05:21 +01:00
Colin Seymour a5b5699b49 Configure Renovate to pin GitHub Actions to SHAs (#147) 2026-06-21 12:55:57 +01:00
16 changed files with 812 additions and 89 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"features": {
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {
"version": "2.1.1",
"resolved": "ghcr.io/eitsupi/devcontainer-features/jq-likes@sha256:6a8b044f9f424097ec6d212167836b26994a76a556cbcb202084be79fe8032bd",
"integrity": "sha256:6a8b044f9f424097ec6d212167836b26994a76a556cbcb202084be79fe8032bd"
},
"ghcr.io/jungaretti/features/ripgrep:1": {
"version": "1.0.1",
"resolved": "ghcr.io/jungaretti/features/ripgrep@sha256:c0922b6f4a9184080c8435b6ef25983dee6c734733f0a749d852ad0d33de2253",
"integrity": "sha256:c0922b6f4a9184080c8435b6ef25983dee6c734733f0a749d852ad0d33de2253"
}
}
}
+14 -4
View File
@@ -1,11 +1,20 @@
{
"name": "HA Addon: TeslaMate",
"image": "ghcr.io/home-assistant/devcontainer:2-addons",
"appPort": ["7123:8123", "7357:4357"],
"image": "ghcr.io/home-assistant/devcontainer:6-apps",
"overrideCommand": false,
"remoteUser": "vscode",
"appPort": ["7123:8123", "7180:80", "7357:4357"],
"postStartCommand": "bash devcontainer_bootstrap",
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
"workspaceFolder": "/mnt/supervisor/addons/local/${localWorkspaceFolderBasename}",
"workspaceMount": "source=${localWorkspaceFolder},target=${containerWorkspaceFolder},type=bind,consistency=cached",
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}",
"SUPERVISOR_CHANNEL": "stable"
},
"features": {
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {},
"ghcr.io/jungaretti/features/ripgrep:1": {}
},
"customizations": {
"vscode": {
@@ -29,6 +38,7 @@
},
"mounts": [
"type=volume,target=/var/lib/docker",
"type=volume,target=/mnt/supervisor"
"type=volume,target=/var/lib/containerd",
"type=volume,target=/mnt/supervisor",
]
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Remove an add-on's registry image before the Supervisor starts so it must
# build the current checkout's Dockerfile.
set -euo pipefail
config="${1:-config.json}"
if [ ! -f "$config" ]; then
echo "ERROR: Add-on config not found: ${config}" >&2
exit 1
fi
if ! jq empty "$config"; then
echo "ERROR: Invalid add-on config: ${config}" >&2
exit 1
fi
if jq -e 'has("image")' "$config" > /dev/null; then
temporary_config=$(mktemp "${config}.XXXXXX")
trap 'rm -f "$temporary_config"' EXIT
jq 'del(.image)' "$config" > "$temporary_config"
mv "$temporary_config" "$config"
fi
if jq -e 'has("image")' "$config" > /dev/null; then
echo "ERROR: Failed to remove image from ${config}" >&2
exit 1
fi
echo "Registry image disabled; the Supervisor will build the current checkout"
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# run-integration-test.sh - Boot the Supervisor and run setup-ha.sh against it.
#
# Usage: bash .devcontainer/run-integration-test.sh [channel]
#
# The channel (stable, beta or dev) selects which Home Assistant release the
# Supervisor installs. It defaults to stable so that CI tests what users actually
# run; the devcontainer image itself defaults to dev, which means unreleased
# Home Assistant changes would otherwise land straight in CI.
#
# Set DEBUG=1 to stream the Supervisor log instead of capturing it.
set -euo pipefail
export SUPERVISOR_CHANNEL="${1:-stable}"
SUPERVISOR_LOG="/tmp/supervisor.log"
bash .devcontainer/prepare-local-addon.sh
# The v6 devcontainer exposes host AppArmor through OS Agent, but profiles
# applied to containers in its nested Docker daemon break the PostgreSQL image.
sudo systemctl stop haos-agent
# supervisor_run hardcodes SUPERVISOR_DEV=1, which makes the Supervisor force its
# update channel to dev and so install nightly Home Assistant builds whatever
# channel is asked for.
sudo sed -i '/-e SUPERVISOR_DEV=1/d' /usr/bin/supervisor_run
# The channel is otherwise only reachable through the Supervisor API, by which
# point it has already installed Home Assistant, so seed it before first boot.
sudo mkdir -p /mnt/supervisor
printf '{"channel": "%s"}\n' "${SUPERVISOR_CHANNEL}" | sudo tee /mnt/supervisor/updater.json > /dev/null
echo "Home Assistant channel: ${SUPERVISOR_CHANNEL}"
if [ -n "${DEBUG:-}" ]; then
supervisor_run 2>&1 | tee "${SUPERVISOR_LOG}" &
else
supervisor_run > "${SUPERVISOR_LOG}" 2>&1 &
fi
if ! bash .devcontainer/setup-ha.sh; then
echo "::group::Supervisor log (last 300 lines, image pull progress omitted)"
grep -v -E "docker_image_pull_update|pull_progress" "${SUPERVISOR_LOG}" | tail -n 300
echo "::endgroup::"
exit 1
fi
+504
View File
@@ -0,0 +1,504 @@
#!/usr/bin/env bash
# setup-ha.sh - Idempotent Home Assistant setup for addon development
#
# This script:
# 1. Completes HA onboarding (creates admin user)
# 2. Adds the alexbelgium addon repository (for PostgreSQL)
# 3. Installs and starts PostgreSQL 17
# 4. Installs, configures, and starts the local addon
#
# Usage: bash .devcontainer/setup-ha.sh
# DEBUG=1 bash .devcontainer/setup-ha.sh # verbose output + error details
#
# Safe to run multiple times (idempotent).
set -euo pipefail
# --- Addon-specific configuration ---
ADDON_NAME="$(jq -r '.name' config.json)"
ADDON_SLUG="local_$(jq -r '.slug' config.json)"
ADDON_PORT="$(jq -r '.ports | keys[0] | split("/") | .[0]' config.json)"
ADDON_READY_LOG="TeslaMateWeb.Endpoint"
TIMEZONE="Europe/London"
# --- Home Assistant configuration ---
HA_HOST="localhost"
# Core binds port 80 under the Supervisor from 2026.8 onwards, and 8123 before
# that, so wait_for_ha resolves the port at runtime rather than assuming one.
HA_PORTS=(80 8123)
HA_URL=""
CLIENT_ID=""
ALEXBELGIUM_REPO="https://github.com/alexbelgium/hassio-addons"
ADMIN_USER="admin"
ADMIN_PASS="pass"
# --- PostgreSQL configuration ---
POSTGRES_PASSWORD="homeassistant"
POSTGRES_USER="postgres"
POSTGRES_DB="${ADDON_NAME,,}"
POSTGRES_PORT=5432
# Addon slug = repo_hash + "_" + addon_slug_from_config
# Repo hash: first 8 chars of sha1("https://github.com/alexbelgium/hassio-addons")
POSTGRES_SLUG="db21ed7f_postgres_latest"
# --- Helpers ---
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
log() { echo -e "${CYAN}==>${NC} ${BOLD}$*${NC}"; }
warn() { echo -e "${YELLOW}WARNING:${NC} $*" >&2; }
err() { echo -e "${RED}ERROR:${NC} $*" >&2; exit 1; }
ok() { echo -e "${CYAN}==>${NC} ${GREEN}$*${NC}"; }
# Parse JSON, aborting with the offending body instead of a bare jq parse error
jq_or_die() {
local json="$1" filter="$2" context="$3"
if [ -z "$json" ] || ! jq empty <<< "$json" 2>/dev/null; then
err "${context}: expected JSON, got: ${json}"
fi
jq -r "$filter" <<< "$json"
}
# Run Supervisor API call via hassio_cli container
# IMPORTANT: Use single quotes for outer sh -c argument so $SUPERVISOR_TOKEN
# is expanded by the inner shell (where the env var exists), not the outer bash.
supervisor_api() {
local method="$1" endpoint="$2"
docker exec hassio_cli sh -c \
'curl -s -X '"${method}"' -H "Authorization: Bearer $SUPERVISOR_TOKEN" -H "Content-Type: application/json" http://supervisor'"${endpoint}"
}
supervisor_api_with_body() {
local method="$1" endpoint="$2" body="$3"
docker exec -e "BODY=${body}" hassio_cli sh -c \
'curl -s -X '"${method}"' -H "Authorization: Bearer $SUPERVISOR_TOKEN" -H "Content-Type: application/json" -d "$BODY" http://supervisor'"${endpoint}"
}
# Query a specific jq field from a Supervisor API response.
# Supervisor reports HTTP errors as plain text (e.g. "404: Not Found"), which is
# not JSON, so surface those and return nothing to let callers keep polling.
supervisor_api_jq() {
local endpoint="$1" jq_filter="$2"
local response
response=$(supervisor_api GET "${endpoint}")
if [ -z "$response" ] || ! jq empty <<< "$response" 2>/dev/null; then
warn "Supervisor API ${endpoint} returned non-JSON: ${response}"
return 0
fi
jq -r "$jq_filter" <<< "$response"
}
# Check if a jq expression matches (returns 0 if match found, 1 otherwise)
supervisor_api_jq_test() {
local endpoint="$1" jq_filter="$2"
docker exec -e "JQ_FILTER=${jq_filter}" hassio_cli sh -c \
'curl -s -H "Authorization: Bearer $SUPERVISOR_TOKEN" http://supervisor'"${endpoint}"' | jq -e "$JQ_FILTER" > /dev/null 2>&1'
}
# --- Step 0: Wait for HA to be ready ---
# Print an addon's log to help diagnose a failure
dump_addon_logs() {
local slug="$1"
echo "::group::${slug} logs"
supervisor_api GET "/addons/${slug}/logs" || true
echo "::endgroup::"
}
# Best-effort Home Assistant Core state from the Supervisor, for diagnostics
ha_core_state() {
local info
info=$(supervisor_api GET "/core/info" 2>/dev/null) || true
if [ -n "$info" ] && jq empty <<< "$info" 2>/dev/null; then
jq -r '.data.state // "unknown"' <<< "$info"
else
echo "unavailable"
fi
}
# HA answers on its HTTP port long before the API is usable, and from 2026.8 the
# port itself moved from 8123 to 80 under the Supervisor. Older releases still use
# 8123, and during onboarding new releases answer on both - but 8123 only serves a
# redirect that Core tears down once onboarding completes. So port 80 is tried
# first, redirects are deliberately not followed, and a port only counts as ready
# when it serves the onboarding payload itself rather than merely valid JSON.
HA_PROBE=""
ha_probe() {
local port="$1" raw status body
raw=$(curl -s --max-time 5 -w $'\n%{http_code}' "http://${HA_HOST}:${port}/api/onboarding" 2>/dev/null || true)
status=${raw##*$'\n'}
body=${raw%$'\n'*}
HA_PROBE="${HA_PROBE:+${HA_PROBE} }port=${port} http=${status:-none} body=${body:0:80}"
jq -e 'type == "array" and (.[0] | has("step"))' <<< "$body" > /dev/null 2>&1
}
# Sets HA_URL and CLIENT_ID on success. Must not run in a subshell.
ha_resolve_url() {
local port
HA_PROBE=""
for port in "${HA_PORTS[@]}"; do
if ha_probe "$port"; then
HA_URL="http://${HA_HOST}:${port}"
CLIENT_ID="${HA_URL}/"
return 0
fi
done
return 1
}
wait_for_ha() {
log "Waiting for Home Assistant to be ready..."
local max_attempts=120
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
if ha_resolve_url; then
ok "Home Assistant is ready at ${HA_URL}"
return 0
fi
if [ $((attempt % 6)) -eq 0 ]; then
log " attempt ${attempt}/${max_attempts}: core=$(ha_core_state) ${HA_PROBE}"
fi
sleep 5
attempt=$((attempt + 1))
done
err "Home Assistant did not become ready in time (core=$(ha_core_state) ${HA_PROBE})"
}
# --- Step 1: Complete onboarding ---
complete_onboarding() {
log "Checking onboarding status..."
local onboarding
onboarding=$(curl -sfL "${HA_URL}/api/onboarding")
local user_done
user_done=$(jq_or_die "$onboarding" '.[] | select(.step == "user") | .done' "Onboarding status")
if [ "$user_done" = "true" ]; then
ok "Onboarding already completed"
return 0
fi
log "Creating admin user '${ADMIN_USER}'..."
local auth_response
auth_response=$(curl -sfL -X POST "${HA_URL}/api/onboarding/users" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"${CLIENT_ID}\",\"name\":\"Admin\",\"username\":\"${ADMIN_USER}\",\"password\":\"${ADMIN_PASS}\",\"language\":\"en\"}")
local auth_code
auth_code=$(jq_or_die "$auth_response" '.auth_code' "Create admin user")
if [ -z "$auth_code" ] || [ "$auth_code" = "null" ]; then
err "Failed to create user: ${auth_response}"
fi
# Exchange auth code for access token
local token_response
token_response=$(curl -sfL -X POST "${HA_URL}/auth/token" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=${auth_code}" \
--data-urlencode "client_id=${CLIENT_ID}")
local access_token
access_token=$(jq_or_die "$token_response" '.access_token' "Token exchange")
if [ -z "$access_token" ] || [ "$access_token" = "null" ]; then
err "Failed to get access token: ${token_response}"
fi
# Complete remaining onboarding steps
log "Completing onboarding steps..."
curl -sfL -X POST "${HA_URL}/api/onboarding/core_config" \
-H "Authorization: Bearer ${access_token}" \
-H "Content-Type: application/json" \
-d "{}" > /dev/null 2>&1 || true
curl -sfL -X POST "${HA_URL}/api/onboarding/analytics" \
-H "Authorization: Bearer ${access_token}" \
-H "Content-Type: application/json" \
-d "{}" > /dev/null 2>&1 || true
curl -sfL -X POST "${HA_URL}/api/onboarding/integration" \
-H "Authorization: Bearer ${access_token}" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"${CLIENT_ID}\",\"redirect_uri\":\"${HA_URL}/\"}" > /dev/null 2>&1 || true
ok "Onboarding complete"
}
# --- Step 2: Add alexbelgium addon repository ---
add_addon_repo() {
log "Checking addon repositories..."
if supervisor_api_jq_test "/store/repositories" ".data[] | select(.source == \"${ALEXBELGIUM_REPO}\")"; then
ok "Repository already added: ${ALEXBELGIUM_REPO}"
return 0
fi
log "Adding addon repository: ${ALEXBELGIUM_REPO}"
supervisor_api_with_body POST "/store/repositories" \
"{\"repository\":\"${ALEXBELGIUM_REPO}\"}" > /dev/null
# Wait for the store to refresh by checking if the postgres addon is available
log "Waiting for store to refresh..."
local max_attempts=60
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
if supervisor_api_jq_test "/addons/${POSTGRES_SLUG}/info" '.data.name'; then
ok "Store refreshed successfully"
return 0
fi
sleep 5
attempt=$((attempt + 1))
done
err "Store did not refresh in time - postgres addon not found"
}
# --- Step 3: Install PostgreSQL ---
install_postgres() {
local postgres_slug="$1"
log "Checking PostgreSQL addon status (${postgres_slug})..."
local version
version=$(supervisor_api_jq "/addons/${postgres_slug}/info" '.data.version // empty')
if [ -n "$version" ]; then
local state
state=$(supervisor_api_jq "/addons/${postgres_slug}/info" '.data.state')
ok "PostgreSQL addon already installed (version: ${version}, state: ${state})"
return 0
fi
log "Installing PostgreSQL addon..."
supervisor_api POST "/addons/${postgres_slug}/install" > /dev/null
# Wait for installation to complete
log "Waiting for PostgreSQL installation..."
local max_attempts=120
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
version=$(supervisor_api_jq "/addons/${postgres_slug}/info" '.data.version // empty')
if [ -n "$version" ]; then
ok "PostgreSQL addon installed (version: ${version})"
return 0
fi
sleep 5
attempt=$((attempt + 1))
done
err "PostgreSQL addon installation timed out"
}
# --- Step 4: Configure and start PostgreSQL ---
configure_and_start_postgres() {
local postgres_slug="$1"
log "Configuring PostgreSQL addon..."
local options_json
options_json=$(jq -n \
--arg pw "$POSTGRES_PASSWORD" \
--arg db "$POSTGRES_DB" \
'{options: {POSTGRES_PASSWORD: $pw, POSTGRES_DB: $db, env_vars: []}}')
supervisor_api_with_body POST "/addons/${postgres_slug}/options" \
"${options_json}" > /dev/null
# Check if already running
local state
state=$(supervisor_api_jq "/addons/${postgres_slug}/info" '.data.state')
if [ "$state" = "started" ]; then
ok "PostgreSQL addon already running"
return 0
fi
log "Starting PostgreSQL addon..."
supervisor_api POST "/addons/${postgres_slug}/start" > /dev/null
# Wait for postgres to be ready
log "Waiting for PostgreSQL to be ready..."
local max_attempts=60
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
state=$(supervisor_api_jq "/addons/${postgres_slug}/info" '.data.state')
if [ "$state" = "started" ]; then
ok "PostgreSQL addon is running"
wait_for_postgres_ready "$postgres_slug"
return 0
fi
sleep 5
attempt=$((attempt + 1))
done
err "PostgreSQL addon did not start in time"
}
wait_for_postgres_ready() {
local postgres_slug="$1"
# Supervisor renamed add-on containers from addon_* to app_*, so try both.
local containers=("app_${postgres_slug}" "addon_${postgres_slug}")
log "Waiting for PostgreSQL database to accept connections..."
local max_attempts=60
local attempt=0
local container probe
while [ "$attempt" -lt "$max_attempts" ]; do
for container in "${containers[@]}"; do
if probe=$(docker exec -e "PGPASSWORD=${POSTGRES_PASSWORD}" "$container" \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT 1" 2>&1); then
ok "PostgreSQL database is ready"
return 0
fi
done
sleep 5
attempt=$((attempt + 1))
done
warn "Last psql output: ${probe}"
dump_addon_logs "$postgres_slug"
err "PostgreSQL database did not become ready in time"
}
# --- Step 5: Install and configure addon ---
install_addon() {
log "Checking ${ADDON_NAME} addon status..."
local version
version=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.version // empty')
if [ -n "$version" ]; then
local state
state=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.state')
ok "${ADDON_NAME} addon already installed (version: ${version}, state: ${state})"
return 0
fi
log "Installing ${ADDON_NAME} addon..."
supervisor_api POST "/addons/${ADDON_SLUG}/install" > /dev/null
# Wait for installation to complete (the TeslaMate image is large)
log "Waiting for ${ADDON_NAME} installation..."
local max_attempts=120
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
version=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.version // empty')
if [ -n "$version" ]; then
ok "${ADDON_NAME} addon installed (version: ${version})"
return 0
fi
sleep 5
attempt=$((attempt + 1))
done
err "${ADDON_NAME} addon installation timed out"
}
configure_and_start_addon() {
local postgres_slug="$1"
# Derive the postgres hostname from its slug (underscores → hyphens)
local postgres_host
postgres_host=$(echo "$postgres_slug" | tr '_' '-')
log "Configuring ${ADDON_NAME} addon (db host: ${postgres_host})..."
local current_options overrides options_json
current_options=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.options')
overrides=$(jq -n \
--arg user "$POSTGRES_USER" \
--arg pass "$POSTGRES_PASSWORD" \
--argjson port "$POSTGRES_PORT" \
--arg host "$postgres_host" \
--arg db "$POSTGRES_DB" \
--arg tz "$TIMEZONE" \
'{database_user: $user, database_pass: $pass, database_port: $port, database_host: $host, database_name: $db, database_ssl: false, disable_mqtt: true, grafana_import_dashboards: false, timezone: $tz}')
options_json=$(jq -n --argjson cur "$current_options" --argjson ovr "$overrides" '{options: ($cur + $ovr)}')
supervisor_api_with_body POST "/addons/${ADDON_SLUG}/options" \
"${options_json}" > /dev/null
# Check if already running
local state
state=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.state')
if [ "$state" = "started" ]; then
ok "${ADDON_NAME} addon already running"
wait_for_addon_ready
return 0
fi
log "Starting ${ADDON_NAME} addon..."
supervisor_api POST "/addons/${ADDON_SLUG}/start" > /dev/null
# Wait for addon state to become "started"
log "Waiting for ${ADDON_NAME} addon to start..."
local max_attempts=60
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
state=$(supervisor_api_jq "/addons/${ADDON_SLUG}/info" '.data.state')
if [ "$state" = "started" ]; then
break
fi
sleep 5
attempt=$((attempt + 1))
done
if [ "$state" != "started" ]; then
dump_addon_logs "${ADDON_SLUG}"
err "${ADDON_NAME} addon did not start in time (state: ${state})"
fi
wait_for_addon_ready
ok "${ADDON_NAME} addon started and ready"
}
wait_for_addon_ready() {
log "Waiting for ${ADDON_NAME} to be ready (listening on port ${ADDON_PORT})..."
local max_attempts=60
local attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
local logs
logs=$(supervisor_api GET "/addons/${ADDON_SLUG}/logs" 2>/dev/null || true)
if echo "$logs" | grep -qF "$ADDON_READY_LOG"; then
ok "${ADDON_NAME} is ready"
return 0
fi
sleep 5
attempt=$((attempt + 1))
done
dump_addon_logs "${ADDON_SLUG}"
err "${ADDON_NAME} did not become ready in time (never saw '${ADDON_READY_LOG}' in logs)"
}
# --- Main ---
main() {
log "Setting up Home Assistant for ${ADDON_NAME} development"
echo ""
wait_for_ha
complete_onboarding
add_addon_repo
install_postgres "$POSTGRES_SLUG"
configure_and_start_postgres "$POSTGRES_SLUG"
install_addon
configure_and_start_addon "$POSTGRES_SLUG"
echo ""
ok "Setup complete!"
local host_port
# Mirrors the appPort list in devcontainer.json.
case "${HA_URL##*:}" in
80) host_port=7180 ;;
*) host_port=7123 ;;
esac
log " Home Assistant: ${HA_URL} (user: ${ADMIN_USER}, pass: ${ADMIN_PASS})"
log " from the host: http://localhost:${host_port}"
log " PostgreSQL: ${POSTGRES_SLUG} (user: ${POSTGRES_USER}, db: ${POSTGRES_DB})"
log " ${ADDON_NAME}: accessible via Home Assistant ingress"
}
main "$@"
+4 -4
View File
@@ -2,7 +2,8 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard"
":disableDependencyDashboard",
"helpers:pinGitHubActionDigests"
],
"prHourlyLimit": 4,
"prBodyTemplate": "{{{table}}}{{{warnings}}}{{{notes}}}{{{changelogs}}}",
@@ -12,10 +13,9 @@
"customManagers": [
{
"customType": "regex",
"fileMatch": "^build.json$",
"fileMatch": "(^|/)Dockerfile$",
"matchStrings": [
"\"teslamate/teslamate:(?<currentValue>\\d+\\.\\d+\\.\\d+)\"",
"\"teslamate_version\": \"(?<currentValue>\\d+\\.\\d+\\.\\d+)\""
"ARG teslamate_version=(?<currentValue>\\d+\\.\\d+\\.\\d+)"
],
"datasourceTemplate": "github-releases",
"depNameTemplate": "teslamate-org/teslamate"
+58
View File
@@ -0,0 +1,58 @@
name: Integration Test
on:
push:
branches:
- main
paths:
- config.json
- Dockerfile
- rootfs/**
- .devcontainer/**
pull_request:
branches:
- main
paths:
- config.json
- Dockerfile
- rootfs/**
- .devcontainer/**
# Home Assistant and the third-party add-on repository this test installs both
# move independently of this repo, so the test can break with no change here.
# Run it on a schedule to catch that on main rather than on an unrelated pull
# request that happens to touch one of the paths above.
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
jobs:
integration:
name: Integration test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run integration test in devcontainer
uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450
with:
runCmd: bash .devcontainer/run-integration-test.sh stable
# Early warning for Home Assistant changes that have not reached stable yet.
# Deliberately never runs on a pull request, so a broken beta cannot block a
# merge, and is non-blocking when it does run.
canary:
name: Integration test (beta)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 30
continue-on-error: true
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run integration test in devcontainer
uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450
with:
runCmd: bash .devcontainer/run-integration-test.sh beta
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⤵️ Check out code from GitHub
uses: actions/checkout@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: 🚀 Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2
uses: frenck/action-addon-linter@b9cfd1bc62ba60d2c4fc96317b5f7ada5923f4a7 # v2.21.1
with:
path: "./"
+62 -27
View File
@@ -11,41 +11,76 @@ on:
type: string
jobs:
publish:
name: Publish
init:
name: Initialize build
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Checkout the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Get version
id: version
run: |
version="${{ inputs.version }}"
if [ -n "$version" ]; then
echo "version=${version}" >> "$GITHUB_OUTPUT"
else
echo "version=$(curl --header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --silent "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest" | jq -r .tag_name | sed s/v//)" >> "$GITHUB_OUTPUT"
fi
- name: Build matrix
id: matrix
run: |
# Derive the per-arch build matrix from config.json. amd64 and aarch64
# build on native runners (no QEMU). Base image, build args and labels
# all live in the Dockerfile, so nothing extra is needed here.
image_template=$(jq -r '.image' config.json)
architectures=$(jq -r '.arch[]' config.json)
matrix='{"include":[]}'
for arch in $architectures; do
case "${arch}" in
amd64) os="ubuntu-24.04" ;;
aarch64) os="ubuntu-24.04-arm" ;;
*) echo "::error::Unsupported architecture: ${arch}"; exit 1 ;;
esac
image="${image_template//\{arch\}/${arch}}"
matrix=$(jq -c \
--arg arch "$arch" \
--arg os "$os" \
--arg image "$image" \
'.include += [{arch: $arch, os: $os, image: $image}]' <<< "${matrix}")
done
echo "matrix=${matrix}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.arch }} image
needs: init
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
matrix:
arch: ["amd64", "aarch64"]
fail-fast: false
matrix: ${{ fromJSON(needs.init.outputs.matrix) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v7.0.0
- name: Get version
run: |
version=${{ inputs.version }}
if [ -n "$version" ]; then
echo "latest_release=$version" >> $GITHUB_ENV
else
echo "latest_release=$(curl --header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --silent https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r .tag_name | sed s/v// )" >> $GITHUB_ENV
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build and publish
uses: home-assistant/builder@2026.03.2
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
with:
args: |
--${{ matrix.arch }} \
--version ${{ env.latest_release }} \
--target /data/ \
--addon
arch: ${{ matrix.arch }}
image: ${{ matrix.image }}
image-tags: |
${{ needs.init.outputs.version }}
latest
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
push: "true"
cosign: "false"
version: ${{ needs.init.outputs.version }}
+4 -4
View File
@@ -19,7 +19,7 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: teslamate
token: ${{ secrets.GITHUB_TOKEN }}
@@ -28,7 +28,7 @@ jobs:
echo "latest_release=$(curl --header "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --silent https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r .tag_name | sed s/v// )" >> $GITHUB_ENV
- name: Increment semver
id: semver
uses: matt-FFFFFF/simple-semver@v0.1.1
uses: matt-FFFFFF/simple-semver@967d66a65b0c5afae2a815ab2d311f66a32293ab # v0.1.1
with:
semver-input: ${{ env.latest_release }}
increment: ${{ github.event.inputs.semverIncrement }}
@@ -52,7 +52,7 @@ jobs:
git push --tags
working-directory: ${{ github.workspace }}/teslamate
- name: Create release
uses: softprops/action-gh-release@v3.0.0
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: v${{ steps.semver.outputs.semver }}
body_path: CHANGELOG.txt
@@ -61,7 +61,7 @@ jobs:
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout addon repo
uses: actions/checkout@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: repo
repository: lildude/ha-addons
+52 -18
View File
@@ -5,9 +5,6 @@ on:
branches:
- main
paths:
- build.yaml
- config.yaml
- build.json
- config.json
- Dockerfile
- rootfs/**
@@ -15,34 +12,71 @@ on:
branches:
- main
paths:
- build.yaml
- config.yaml
- build.json
- config.json
- Dockerfile
- rootfs/**
jobs:
build:
init:
name: Initialize build
runs-on: ubuntu-latest
name: Build add-on
permissions:
contents: read
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build matrix
id: matrix
run: |
# Derive the per-arch build matrix from config.json. amd64 and aarch64
# build on native runners (no QEMU). Base image, build args and labels
# all live in the Dockerfile, so nothing extra is needed here.
image_template=$(jq -r '.image' config.json)
architectures=$(jq -r '.arch[]' config.json)
matrix='{"include":[]}'
for arch in $architectures; do
case "${arch}" in
amd64) os="ubuntu-24.04" ;;
aarch64) os="ubuntu-24.04-arm" ;;
*) echo "::error::Unsupported architecture: ${arch}"; exit 1 ;;
esac
image="${image_template//\{arch\}/${arch}}"
matrix=$(jq -c \
--arg arch "$arch" \
--arg os "$os" \
--arg image "$image" \
'.include += [{arch: $arch, os: $os, image: $image}]' <<< "${matrix}")
done
echo "matrix=${matrix}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.arch }} add-on
needs: init
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
matrix:
arch: ["amd64", "aarch64"]
fail-fast: false
matrix: ${{ fromJSON(needs.init.outputs.matrix) }}
steps:
- name: Check out repository
uses: actions/checkout@v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build add-on
uses: home-assistant/builder@2026.03.2
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
with:
args: |
--test \
--${{ matrix.arch }} \
--target /data/ \
--addon
arch: ${{ matrix.arch }}
image: ${{ matrix.image }}
image-tags: test
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
load: "true"
cosign: "false"
version: test
- name: Image labels
run: |
docker inspect ghcr.io/lildude/ha-addon-teslamate-${{ matrix.arch }} --format='{{json .Config.Labels}}' | jq
docker inspect ${{ matrix.image }}:test --format='{{json .Config.Labels}}' | jq
-6
View File
@@ -1,11 +1,5 @@
{
"json.schemas": [
{
"fileMatch": [
"**/build.json"
],
"url": "https://raw.githubusercontent.com/frenck/action-addon-linter/main/src/build.schema.json"
},
{
"fileMatch": [
"**/config.json"
+12 -5
View File
@@ -1,11 +1,9 @@
ARG BUILD_FROM
ARG teslamate_version
FROM teslamate/grafana:${teslamate_version} as grafana
ARG teslamate_version=4.2.0
FROM teslamate/grafana:${teslamate_version} AS grafana
#---
ARG BUILD_FROM
FROM $BUILD_FROM
FROM teslamate/teslamate:${teslamate_version}
ARG BUILD_ARCH
ARG BASHIO_VERSION=0.16.2
@@ -49,5 +47,14 @@ COPY rootfs /
COPY --from=grafana /dashboards /dashboards
COPY --from=grafana /dashboards_internal /dashboards
LABEL \
maintainer="Colin Seymour (https://github.com/lildude)" \
org.opencontainers.image.authors="Colin Seymour (https://github.com/lildude)" \
org.opencontainers.image.description="A self-hosted data logger for your Tesla." \
org.opencontainers.image.documentation="https://github.com/lildude/ha-addon-teslamate/blob/main/DOCS.md" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/lildude/ha-addon-teslamate/" \
org.opencontainers.image.title="Home Assistant Add-on: TeslaMate"
# S6-Overlay
ENTRYPOINT ["/init"]
+1 -1
View File
@@ -85,6 +85,6 @@ Everything should pick up where it was before.
[repo-btn]: https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg
[sponsor-badge]: https://img.shields.io/badge/Sponsor_Me-%E2%9D%A4-ec6cb9?logo=GitHub
[teslamate-backup]: https://docs.teslamate.org/docs/maintenance/backup_restore
[teslamate-version]: https://img.shields.io/badge/dynamic/json?label=TeslaMate%20Version&url=https%3A%2F%2Fraw.githubusercontent.com%2Flildude%2Fha-addon-teslamate%2Fmain%2Fbuild.json&query=%24.args.teslamate_version
[teslamate-version]: https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fgithub.com%2Flildude%2Fha-addon-teslamate%2Fblob%2Fmain%2FDockerfile&search=teslamate_version%3D(%3F%3Ccurrent_ver%3E%5Cd%2B%5C.%5Cd%2B%5C.%5Cd%2B)&replace=%241&label=TeslaMate%20Version
[teslamate]: https://github.com/teslamate-org/teslamate/
[timescaledb]: https://github.com/expaso/hassos-addon-timescaledb
-18
View File
@@ -1,18 +0,0 @@
{
"build_from": {
"aarch64": "teslamate/teslamate:4.0.1",
"amd64": "teslamate/teslamate:4.0.1"
},
"args": {
"teslamate_version": "4.0.1"
},
"labels":{
"maintainer": "Colin Seymour (https://github.com/lildude)",
"org.opencontainers.image.authors": "Colin Seymour (https://github.com/lildude)",
"org.opencontainers.image.description": "A self-hosted data logger for your Tesla.",
"org.opencontainers.image.documentation": "https://github.com/lildude/ha-addon-teslamate/blob/main/DOCS.md",
"org.opencontainers.image.licenses": "MIT",
"org.opencontainers.image.source": "https://github.com/lildude/ha-addon-teslamate/",
"org.opencontainers.image.title": "Home Assistant Add-on: TeslaMate"
}
}
+6
View File
@@ -13,6 +13,12 @@
"init": false,
"panel_icon": "mdi:car-connected",
"panel_title": "TeslaMate",
"ports": {
"4000/tcp": null
},
"ports_description": {
"4000/tcp": "Web interface (not required for Home Assistant ingress)"
},
"options": {
"database_user": null,
"database_pass": null,