Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Tests

on:
push:
pull_request:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements_test.txt
- name: Install test dependencies
run: python -m pip install -r requirements_test.txt
- name: Lint
run: ruff check .
- name: Check formatting
run: ruff format --check .
- name: Test
run: pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
.pytest_cache/
.ruff_cache/
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

Die Einrichtung laeuft komplett ueber die Home-Assistant-Oberflaeche. YAML ist nicht noetig.

Version `1.0.1` ist mit Home Assistant `2026.8` kompatibel und gegen `2026.8.1`
getestet.

## Was Macht Die Integration?

Du richtest einmal deinen Netzbezug-/Einspeise-Leistungssensor ein. Danach kann Solar Load Split passende Geraete-Leistungssensoren erkennen oder manuell hinzufuegen. Optional kannst du pro Geraet auch einen Energiezaehler des gleichen Zwischensteckers oder Shelly auswaehlen.
Expand Down Expand Up @@ -251,7 +254,9 @@ Anzeige der Sensorwerte.
- Die Integration ist HACS-kompatibel.
- Deutsch und Englisch werden ueber Home-Assistant-Translations unterstuetzt.
- Jeder Split-Eintrag hat stabile Unique IDs.
- Bestehende Entitaetsnamen werden von Home Assistant im Entity Registry gespeichert. Wenn du Namen nach einem Update testen willst, loesche den betroffenen Eintrag und fuege ihn neu hinzu oder setze die Namen manuell zurueck.
- Eigene Entitaetsnamen bleiben bei Updates erhalten. Alte automatisch erzeugte Namen
werden einmalig auf das aktuelle Home-Assistant-Uebersetzungssystem migriert; Entity-IDs
und Automationen bleiben dabei unveraendert.

## English Short Version

Expand Down
1 change: 0 additions & 1 deletion custom_components/pv_device_split/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN][entry.entry_id] = entry.data

async_schedule_power_discovery(hass)
async_schedule_power_discovery_retries(hass)

if CONF_DEVICE_POWER not in entry.data:
return True
Expand Down
45 changes: 26 additions & 19 deletions custom_components/pv_device_split/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
Expand Down Expand Up @@ -84,9 +83,7 @@ async def async_step_manual_device(
"""Manually add a device power sensor."""
grid_source_entries = _grid_source_entries(self.hass)
if user_input is not None:
return await self._async_create_split_entry(
_with_grid_defaults(self.hass, user_input)
)
return await self._async_create_split_entry(_with_grid_defaults(self.hass, user_input))

return self.async_show_form(
step_id="manual_device",
Expand Down Expand Up @@ -211,9 +208,7 @@ def _hub_schema(defaults: dict[str, Any] | None = None) -> vol.Schema:
vol.Required(
CONF_GRID_POWER,
default=defaults.get(CONF_GRID_POWER, vol.UNDEFINED),
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
): selector.EntitySelector(selector.EntitySelectorConfig(domain="sensor")),
vol.Optional(
CONF_INVERT_GRID,
default=defaults.get(CONF_INVERT_GRID, False),
Expand Down Expand Up @@ -264,15 +259,11 @@ def _discovery_schema(defaults: dict[str, Any]) -> vol.Schema:
vol.Required(
CONF_DEVICE_POWER,
default=defaults.get(CONF_DEVICE_POWER, vol.UNDEFINED),
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
): selector.EntitySelector(selector.EntitySelectorConfig(domain="sensor")),
vol.Optional(
CONF_DEVICE_ENERGY,
default=defaults.get(CONF_DEVICE_ENERGY, vol.UNDEFINED),
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
): selector.EntitySelector(selector.EntitySelectorConfig(domain="sensor")),
}
)

Expand All @@ -291,9 +282,7 @@ def _manual_device_schema(defaults: dict[str, Any]) -> vol.Schema:
vol.Optional(
CONF_DEVICE_ENERGY,
default=defaults.get(CONF_DEVICE_ENERGY, vol.UNDEFINED),
): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor")
),
): selector.EntitySelector(selector.EntitySelectorConfig(domain="sensor")),
vol.Required(
CONF_GRID_POWER,
default=defaults.get(CONF_GRID_POWER, vol.UNDEFINED),
Expand Down Expand Up @@ -406,14 +395,24 @@ async def async_step_init(
**self._config_entry.data,
**user_input,
}
unique_id = _entry_unique_id(data)
if any(
entry.entry_id != self._config_entry.entry_id and entry.unique_id == unique_id
for entry in self.hass.config_entries.async_entries(DOMAIN)
):
return self.async_show_form(
step_id="init",
data_schema=_options_schema(data),
errors={"base": "already_configured"},
)

self.hass.config_entries.async_update_entry(
self._config_entry,
title=data.get(CONF_NAME, DEFAULT_NAME),
data=data,
unique_id=unique_id,
)
self.hass.create_task(
self.hass.config_entries.async_reload(self._config_entry.entry_id)
)
self.hass.config_entries.async_schedule_reload(self._config_entry.entry_id)
return self.async_create_entry(title="", data={})

return self.async_show_form(
Expand Down Expand Up @@ -485,3 +484,11 @@ def _options_schema(defaults: dict[str, Any]) -> vol.Schema:
] = selector.EntitySelector(selector.EntitySelectorConfig(domain="sensor"))

return vol.Schema(schema)


@callback
def _entry_unique_id(data: dict[str, Any]) -> str:
"""Return the unique ID matching the current entry configuration."""
if CONF_DEVICE_POWER in data:
return _pair_unique_id(data[CONF_DEVICE_POWER], data[CONF_GRID_POWER])
return f"grid_{data[CONF_GRID_POWER]}"
4 changes: 3 additions & 1 deletion custom_components/pv_device_split/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from homeassistant.const import Platform

DOMAIN = "pv_device_split"

CONF_DEVICE_POWER = "device_power"
Expand All @@ -16,4 +18,4 @@
DEFAULT_GRID_BUFFER_SECONDS = 0
DEFAULT_GRID_DEADBAND_WATTS = 100

PLATFORMS = ["sensor"]
PLATFORMS: list[Platform] = [Platform.SENSOR]
7 changes: 5 additions & 2 deletions custom_components/pv_device_split/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from .const import (
CONF_DEVICE_POWER,
CONF_ENABLE_DISCOVERY,
CONF_GRID_POWER,
CONF_GRID_BUFFER_SECONDS,
CONF_GRID_DEADBAND_WATTS,
CONF_GRID_POWER,
CONF_INVERT_GRID,
DEFAULT_GRID_BUFFER_SECONDS,
DEFAULT_GRID_DEADBAND_WATTS,
Expand Down Expand Up @@ -54,7 +54,10 @@ class PowerCandidate:
@callback
def async_schedule_power_discovery(hass: HomeAssistant) -> None:
"""Schedule a scan for useful power sensors."""
hass.create_task(_async_discover_power_pair(hass))
hass.async_create_task(
_async_discover_power_pair(hass),
"Discover Solar Load Split power sensors",
)


@callback
Expand Down
10 changes: 6 additions & 4 deletions custom_components/pv_device_split/manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"domain": "pv_device_split",
"name": "Solar Load Split",
"codeowners": [],
"codeowners": ["@dr-apple"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/custom-components/pv_device_split",
"iot_class": "local_push",
"documentation": "https://github.com/dr-apple/Solar-Load-Split",
"integration_type": "helper",
"iot_class": "calculated",
"issue_tracker": "https://github.com/dr-apple/Solar-Load-Split/issues",
"requirements": [],
"version": "1.0.0"
"version": "1.0.1"
}
Loading
Loading