Add Home Assistant integration discovery

This commit is contained in:
2026-08-10 09:28:42 +02:00
parent f2887a0ab4
commit 224e190fa1
11 changed files with 144 additions and 22 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.2.0
- Register the internal optimizer API through Home Assistant Supervisor
discovery.
- Enable automatic setup of the SolarEdge Optimizers custom integration.
## 0.1.0
- Initial Home Assistant App package.
+13 -4
View File
@@ -83,9 +83,18 @@ im App-Panel erreichbar.
## Zugriff aus Home Assistant
Die App veröffentlicht standardmäßig keinen ungeschützten Port im lokalen
Netz. Home Assistant und andere Apps können sie über das interne App-Netzwerk
erreichen. Bei einer lokalen Installation lautet der Hostname typischerweise
`local-solaredge-optimizer-data`:
Netz. Ab Version 0.2.0 meldet sie ihren internen Host und Port automatisch über
die Supervisor-Diensterkennung. Dadurch kann die
[SolarEdge Optimizers Integration](https://git.jensneuber.de/jens/ha-solaredge-optimizers)
ohne manuelle interne URL eingerichtet werden.
Nach Installation und Start der App erscheint unter **Einstellungen → Geräte &
Dienste** die gefundene Integration **SolarEdge Optimizers**. Installiere die
Custom Integration vorher über HACS und bestätige anschließend den Fund.
Home Assistant und andere Apps können die API außerdem direkt über das interne
App-Netzwerk erreichen. Bei einer lokalen Installation lautet der Hostname
typischerweise `local-solaredge-optimizer-data`:
```text
http://local-solaredge-optimizer-data:8099/api/optimizers
@@ -119,7 +128,7 @@ für eine lokale Installation nach:
Lade anschließend den App Store neu, installiere **SolarEdge Optimizer Data**,
pflege Benutzername, Passwort und Site-ID im Tab **Konfiguration** und starte
die App. Der Supervisor lädt dabei das zur App-Version passende Multi-Arch-Image
`git.jensneuber.de/jens/solaredgeoptimizers:0.1.0`.
`git.jensneuber.de/jens/solaredgeoptimizers:0.2.0`.
## Sicherheit und Betrieb
+4 -1
View File
@@ -1,5 +1,5 @@
name: "SolarEdge Optimizer Data"
version: "0.1.0"
version: "0.2.0"
slug: "solaredge_optimizer_data"
description: >-
Cached daily energy and current power data for every SolarEdge optimizer.
@@ -7,6 +7,9 @@ arch:
- aarch64
- amd64
image: git.jensneuber.de/jens/solaredgeoptimizers
discovery:
- solaredge_optimizers
hassio_api: true
startup: application
boot: auto
init: false
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@solar-dash/solaredge-optimizer-app",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { registerHomeAssistantDiscovery } from "./discovery";
describe("registerHomeAssistantDiscovery", () => {
test("registers the internal App endpoint with Supervisor", async () => {
let requestUrl: string | undefined;
let requestInit: RequestInit | undefined;
const fetchImpl = (async (
input: string | URL | Request,
init?: RequestInit,
) => {
requestUrl = String(input);
requestInit = init;
return Response.json({ result: "ok", data: { uuid: "test-uuid" } });
}) as typeof fetch;
await registerHomeAssistantDiscovery({
port: 8099,
host: "abc-solaredge-optimizer-data",
supervisorToken: "secret-token",
fetchImpl,
});
expect(requestUrl).toBe("http://supervisor/discovery");
expect(requestInit?.method).toBe("POST");
expect(requestInit?.headers).toEqual({
Accept: "application/json",
Authorization: "Bearer secret-token",
"Content-Type": "application/json",
});
expect(JSON.parse(String(requestInit?.body))).toEqual({
service: "solaredge_optimizers",
config: { host: "abc-solaredge-optimizer-data", port: 8099 },
});
});
test("does not start without a Supervisor token", async () => {
await expect(
registerHomeAssistantDiscovery({
port: 8099,
host: "abc-solaredge-optimizer-data",
supervisorToken: "",
}),
).rejects.toThrow("Supervisor token is unavailable");
});
});
+40
View File
@@ -0,0 +1,40 @@
import { hostname as systemHostname } from "node:os";
const DISCOVERY_SERVICE = "solaredge_optimizers";
const SUPERVISOR_DISCOVERY_URL = "http://supervisor/discovery";
export async function registerHomeAssistantDiscovery({
port,
host = process.env.HOSTNAME || systemHostname(),
supervisorToken = process.env.SUPERVISOR_TOKEN,
fetchImpl = fetch,
}: {
port: number;
host?: string;
supervisorToken?: string;
fetchImpl?: typeof fetch;
}): Promise<void> {
if (!host) {
throw new Error("App hostname is unavailable");
}
if (!supervisorToken) {
throw new Error("Supervisor token is unavailable");
}
const response = await fetchImpl(SUPERVISOR_DISCOVERY_URL, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${supervisorToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
service: DISCOVERY_SERVICE,
config: { host, port },
}),
});
if (!response.ok) {
throw new Error(`Supervisor discovery failed with HTTP ${response.status}`);
}
}
+10
View File
@@ -7,6 +7,7 @@ import {
readAppOptions,
resolveHomeAssistantTimeZone,
} from "./config";
import { registerHomeAssistantDiscovery } from "./discovery";
import { createRequestHandler } from "./http";
export async function main(): Promise<void> {
@@ -52,6 +53,15 @@ export async function main(): Promise<void> {
"info",
`Listening on port ${port}; refresh interval ${options.pollIntervalMinutes} minutes; time zone ${timeZone}`,
);
try {
await registerHomeAssistantDiscovery({ port });
log("info", "Registered optimizer API discovery with Home Assistant");
} catch (error) {
log(
"warning",
`Home Assistant discovery registration failed: ${formatStartupError(error)}`,
);
}
cache.start();
}