SHM: make serial configurable (#28198)

This commit is contained in:
Johannes Rudolph 2026-03-14 16:42:31 +01:00 • committed by GitHub
parent d680fff9d0
commit b7fd7e44f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 51 additions and 1 deletions

View file

@ -51,6 +51,22 @@
minlength="12"
maxlength="12"
pattern="[A-Fa-f0-9]{12}"
/>
</FormRow>
<FormRow
id="shmDeviceserial"
:label="$t('config.shm.labelDeviceSerial')"
:help="$t('config.shm.descriptionDeviceSerial')"
example="CCCCCCCCCCCC"
optional
>
<input
id="shmDeviceserial"
v-model="values.deviceSerial"
class="form-control"
minlength="12"
maxlength="12"
pattern="[A-Fa-f0-9]{12}"
/> </FormRow
></template>
</PropertyCollapsible>

View file

@ -44,6 +44,7 @@ type Config struct {
AllowControl_ bool `json:"allowControl,omitempty"` // deprecated
VendorId string `json:"vendorId"`
DeviceId string `json:"deviceId"`
DeviceSerial string `json:"deviceSerial"`
}
// NewFromConfig creates a new SEMP instance from configuration and starts it
@ -60,6 +61,24 @@ func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, route
return err
}
// Only if DeviceSerial is explicitly configured: validate it and patch the
// UUID node (last 6 bytes) to ensure the UDN and DeviceSerial are stable across restarts.
// Ideally we'd have used the same machine-id approach as UniqueDeviceID does, but that
// would break existing installations that relied on the node ID of the UUID which was set to the MAC address
// of the host. See https://github.com/evcc-io/evcc/issues/28126 for context.
if cfg.DeviceSerial != "" {
b, err := hex.DecodeString(cfg.DeviceSerial)
if err != nil {
return fmt.Errorf("device serial: %w", err)
}
if len(b) != 6 {
return fmt.Errorf("invalid device serial: %v. Must be 12 characters HEX string", cfg.DeviceSerial)
}
// replaces the node (last 6 bytes) of a UUID with the given bytes.
copy(uid[10:], b)
}
var did []byte
if cfg.DeviceId == "" {
if did, err = UniqueDeviceID(); err != nil {

View file

@ -640,11 +640,13 @@
"cardTitle": "Sunny Home Manager",
"description": "evcc ist mit einer Integration für den SMA Sunny Home Manager (SHM) mittels SEMP-Protokoll ausgestattet. Wenn dieser im selben Netzwerk läuft, sollte dir nach der Anmeldung in deinem Sunny Portal-Konto automatisch angeboten werden, alle in evcc konfigurierten Ladepunkte als neu erkannte Verbraucher hinzuzufügen. Alles sollte sofort einsatzbereit sein, ohne dass hier weitere Anpassungen erforderlich sind.",
"descriptionDeviceId": "12-stelliger HEX-String. Präfix für alle Geräte (Ladepunkt, ..).",
"descriptionDeviceSerial": "12-stelliger HEX-String. Basisseriennummer für alle Geräte (Ladepunkt, ..). Standardmäßig leitet evcc diese aus der MAC-Addresse des Hosts ab.",
"descriptionIdPattern": "Kennungsmuster",
"descriptionIds": "Im Sunny Portal benötigt jeder Verbraucher (Ladepunkt, ..) eine eindeutige Kennung. evcc generiert basierend auf deiner Hardware eine eindeutige Kennung. Wenn du evcc auf andere Hardware migrierst, kann sich diese Kennung ändern. Um die Historie zu erhalten, kannst du die generierten Kennungen hier überschreiben. Öffne die SEMP-URL (/semp), um deine aktuellen Kennungen zu überprüfen.",
"descriptionSempUrl": "SEMP-URL",
"descriptionVendorId": "8-stelliger HEX-String. Allgemeines Präfix aller Entitäten. Standardmäßig verwendet evcc seine eigene interne Hersteller-ID.",
"labelDeviceId": "Geräte-ID",
"labelDeviceSerial": "Geräteseriennummer",
"labelVendorId": "Hersteller-ID",
"title": "SMA Sunny Home Manager"
},

View file

@ -640,11 +640,13 @@
"cardTitle": "Sunny Home Manager",
"description": "evcc is equipped with integration for the SMA Sunny Home Manager (SHM) via SEMP protocol. If it is running on the same network, after logging into your Sunny Portal account, you should automatically be offered to add all chargers configured in evcc as newly discovered consumers. Everything should be ready to use immediately, without any adjustments required below.",
"descriptionDeviceId": "12 characters HEX string. Prefix for all devices (charging point, ..).",
"descriptionDeviceSerial": "12 characters HEX string. Base serial for all devices (charging point, ..). By default evcc derives this from the MAC address of the host.",
"descriptionIdPattern": "Identifier pattern",
"descriptionIds": "In Sunny Portal every consumer device needs a unique identifier. evcc generates a unique identifier based on your hardware. If you migrate evcc to another hardware these identifiers might change. If you want to maintain history you can override the generated identifiers here. Open the SEMP URL (/semp) to check your current identifiers.",
"descriptionSempUrl": "SEMP URL",
"descriptionVendorId": "8 characters HEX string. General prefix of all entities. By default evcc will use its own internal vendor ID.",
"labelDeviceId": "Device ID",
"labelDeviceSerial": "Device Serial",
"labelVendorId": "Vendor ID",
"title": "SMA Sunny Home Manager"
},

View file

@ -11,8 +11,10 @@ test.afterEach(async () => {
const CONFIG = "basics.evcc.yaml";
const VALID_VENDOR_ID = "ABCD1234";
const VALID_DEVICE_ID = "1234567890AB";
const VALID_DEVICE_SERIAL = "AABBCCDDEEFF";
const INVALID_VENDOR_ID = "INVALID";
const INVALID_DEVICE_ID = "NOTVALID";
const INVALID_DEVICE_SERIAL = "NOTVALID";
test.describe("SHM", () => {
test("configure SHM with validation and persistence", async ({ page }) => {
@ -43,6 +45,13 @@ test.describe("SHM", () => {
await device.fill(VALID_DEVICE_ID);
expect(await device.evaluate((el: HTMLInputElement) => el.validity.valid)).toBe(true);
// test device serial validation
const serial = modal.getByLabel("Device Serial");
await serial.fill(INVALID_DEVICE_SERIAL);
expect(await serial.evaluate((el: HTMLInputElement) => el.validity.valid)).toBe(false);
await serial.fill(VALID_DEVICE_SERIAL);
expect(await serial.evaluate((el: HTMLInputElement) => el.validity.valid)).toBe(true);
await modal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(modal);
@ -54,8 +63,9 @@ test.describe("SHM", () => {
await expectModalVisible(modal);
await expect(vendor).toHaveValue(VALID_VENDOR_ID);
await expect(device).toHaveValue(VALID_DEVICE_ID);
await expect(serial).toHaveValue(VALID_DEVICE_SERIAL);
// verify SEMP endpoint contains configured IDs
// verify SEMP endpoint contains configured IDs and serial
const [sempPage] = await Promise.all([
page.context().waitForEvent("page"),
modal.getByTestId("semp-url").click(),
@ -64,6 +74,7 @@ test.describe("SHM", () => {
expect(xml).toContain(
`<DeviceId>F-${VALID_VENDOR_ID}-${VALID_DEVICE_ID.toLowerCase()}-00</DeviceId>`
);
expect(xml).toContain(`<DeviceSerial>${VALID_DEVICE_SERIAL.toLowerCase()}-0</DeviceSerial>`);
await sempPage.close();
});
});