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
8 changes: 4 additions & 4 deletions custom_components/solisconnect/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
vol.Required("has_parallel", default=False): bool,
vol.Required("has_battery", default=True): bool,
vol.Required("has_hv_battery", default=False): bool,
vol.Required("has_generator", default=True): bool,
vol.Required("has_generator", default=False): bool,
}

# Combined schema that accepts both TCP and Serial fields (all optional except connection_type)
Expand Down Expand Up @@ -142,7 +142,7 @@
vol.Required("has_parallel", default=False): bool,
vol.Required("has_battery", default=True): bool,
vol.Required("has_hv_battery", default=False): bool,
vol.Required("has_generator", default=True): bool,
vol.Required("has_generator", default=False): bool,
}

OPTIONS_SCHEMA = vol.Schema(
Expand All @@ -159,7 +159,7 @@
vol.Required("has_parallel", default=False): bool,
vol.Required("has_battery", default=True): bool,
vol.Required("has_hv_battery", default=False): bool,
vol.Required("has_generator", default=True): bool,
vol.Required("has_generator", default=False): bool,
}
)

Expand Down Expand Up @@ -675,7 +675,7 @@ def async_get_options_flow(config_entry):
vol.Required("has_parallel", default=False): bool,
vol.Required("has_battery", default=True): bool,
vol.Required("has_hv_battery", default=False): bool,
vol.Required("has_generator", default=True): bool,
vol.Required("has_generator", default=False): bool,
}
)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/solisconnect/data/solis_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def inverter_options_from_config(config: dict, template: InverterConfig) -> Inve
pv=config.get("has_pv", template.type in (InverterType.HYBRID, InverterType.GRID, InverterType.WAVESHARE)),
ac_coupling=config.get("has_ac_coupling", False),
parallel=config.get("has_parallel", False),
generator=config.get("has_generator", True),
generator=config.get("has_generator", False),
battery=config.get("has_battery", True),
hv_battery=config.get("has_hv_battery", False),
)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/sensor_data/hybrid_sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
"category": Category.BASIC_INFORMATION,
"register": ["33001"],
"multiplier": 0,
"value_format": "hex",
},
{
"name": "HMI Version",
"unique": "solisconnect_inverter_hmi_version",
"category": Category.BASIC_INFORMATION,
"register": ["33002"],
"multiplier": 0,
"value_format": "hex",
},
{
"name": "Protocol Version",
Expand Down
375 changes: 199 additions & 176 deletions custom_components/solisconnect/sensor_data/switch_sensors.py

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions custom_components/solisconnect/sensors/entity_category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from homeassistant.helpers.entity import EntityCategory

from custom_components.solisconnect.data.enums import Category

SETTING_CATEGORIES = {
Category.BASIC_SETTING,
Category.BATTERY_SETTING,
Category.AC_PORT_SETTING,
Category.GRID_CODE_SETTING,
Category.POWER_CONTROL_SETTING,
Category.FUNCTIONAL_SETTING,
Category.HYBRID_MODE_SETTING,
Category.BACKUP_PORT_SETTING,
Category.REMOTE_CONTROL_SETTING,
Category.SMART_PORT_SETTING,
Category.GENERATOR_SETTING,
Category.HISTORY_DATA_QUERY_SETTING,
Category.REMOTE_DISPATCH_SETTING,
}


def entity_category_for_sensor_category(category: Category | None) -> EntityCategory | None:
if category in SETTING_CATEGORIES:
return EntityCategory.CONFIG
return None
7 changes: 6 additions & 1 deletion custom_components/solisconnect/sensors/solis_base_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def __init__(
identification=None,
poll_speed=PollSpeed.NORMAL,
data_type: str | None = None,
value_format: str | None = None,
):
"""
:param name: Sensor name
Expand Down Expand Up @@ -90,6 +91,7 @@ def __init__(
self.poll_speed = poll_speed
self.category = category
self.identification = identification
self.value_format = value_format

self.dynamic_adjustments()

Expand Down Expand Up @@ -169,7 +171,9 @@ def _convert_raw_value(self, values: list[int]):
if getattr(self, "data_type", None) == DataType.S16.value and raw > 32767:
raw -= 65536

if self.multiplier == 0 or self.multiplier == 1:
if self.value_format == "hex":
n_value = f"0x{raw & 0xFFFF:04X}"
elif self.multiplier == 0 or self.multiplier == 1:
n_value = round(raw)
else:
n_value = raw * self.multiplier
Expand Down Expand Up @@ -229,6 +233,7 @@ def __init__(self, hass, definition, controller, identification=None):
default=entity.get("default", 0),
multiplier=entity.get("multiplier", 1),
data_type=entity.get("data_type", None),
value_format=entity.get("value_format", None),
unique_id=unique_id_generator(controller, entity),
poll_speed=definition.get("poll_speed", PollSpeed.NORMAL),
),
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/sensors/solis_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.restore_state import RestoreEntity

from custom_components.solisconnect import ModbusController
Expand Down Expand Up @@ -36,6 +37,7 @@ def __init__(self, hass, modbus_controller, entity_definition):
self._attr_unique_id = unique_id_generator_binary(modbus_controller, self._register, self._bit_position, self._on_value)
self._attr_name = entity_definition["name"]
self._attr_available = False
self._attr_entity_category = EntityCategory.CONFIG

async def async_added_to_hass(self) -> None:
"""Called when entity is added to HA."""
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/sensors/solis_number_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from custom_components.solisconnect.const import REGISTER, VALUE
from custom_components.solisconnect.helpers import cache_get, is_correct_controller, register_update_signal
from custom_components.solisconnect.sensors.entity_category import entity_category_for_sensor_category
from custom_components.solisconnect.sensors.solis_base_sensor import SolisBaseSensor

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -43,6 +44,7 @@ def __init__(self, hass, sensor: SolisBaseSensor):
self._attr_state_class = sensor.state_class
self._attr_native_unit_of_measurement = sensor.unit_of_measurement
self._attr_available = not sensor.hidden and sensor.enabled
self._attr_entity_category = entity_category_for_sensor_category(getattr(sensor, "category", None))

self._received_values = {}

Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/sensors/solis_select_entity.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from homeassistant.components.select import SelectEntity
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.restore_state import RestoreEntity

from custom_components.solisconnect import ModbusController
Expand All @@ -16,6 +17,7 @@ def __init__(self, hass, modbus_controller, entity_definition) -> None:
self._register = entity_definition["register"]
self._attr_name = entity_definition["name"]
self._attr_unique_id = unique_id_generator(modbus_controller, entity_definition["register"], "select")
self._attr_entity_category = EntityCategory.CONFIG

self._attr_options = [e["name"] for e in entity_definition["entities"]]
self._attr_options_raw = entity_definition["entities"]
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/sensors/solis_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from custom_components.solisconnect.const import REGISTER, VALUE
from custom_components.solisconnect.data.enums import InverterType, PollSpeed
from custom_components.solisconnect.helpers import cache_get, is_correct_controller, register_update_signal
from custom_components.solisconnect.sensors.entity_category import entity_category_for_sensor_category
from custom_components.solisconnect.sensors.solis_base_sensor import SolisBaseSensor

_LOGGER = logging.getLogger(__name__)
Expand All @@ -34,6 +35,7 @@ def __init__(self, hass: HomeAssistant, sensor: SolisBaseSensor):
self._attr_native_unit_of_measurement = sensor.unit_of_measurement
self._attr_available = not sensor.hidden and sensor.enabled
self._attr_suggested_display_precision = self.decimal_count(sensor.multiplier)
self._attr_entity_category = entity_category_for_sensor_category(getattr(sensor, "category", None))

self.is_added_to_hass = False
self._state = None
Expand Down
2 changes: 2 additions & 0 deletions custom_components/solisconnect/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory

from custom_components.solisconnect import ModbusController
from custom_components.solisconnect.const import DOMAIN, REGISTER, TIME_ENTITIES, VALUE
Expand Down Expand Up @@ -54,6 +55,7 @@ def __init__(self, hass, modbus_controller, entity_definition):
self._attr_has_entity_name = True
self._attr_available = True
self._attr_device_class = entity_definition.get("device_class", None)
self._attr_entity_category = EntityCategory.CONFIG
self._attr_available = True

self._received_values = {}
Expand Down
15 changes: 0 additions & 15 deletions tests/known_sensors_host.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,7 @@
"solisconnect_1.2.3.4_43302_4",
"solisconnect_1.2.3.4_43302_8",
"solisconnect_1.2.3.4_43302_9",
"solisconnect_1.2.3.4_43340_0",
"solisconnect_1.2.3.4_43340_1",
"solisconnect_1.2.3.4_43363_1",
"solisconnect_1.2.3.4_43365_0",
"solisconnect_1.2.3.4_43365_1",
"solisconnect_1.2.3.4_43365_2",
"solisconnect_1.2.3.4_43365_3",
"solisconnect_1.2.3.4_43365_4",
"solisconnect_1.2.3.4_43483_0",
"solisconnect_1.2.3.4_43483_1",
"solisconnect_1.2.3.4_43483_2",
"solisconnect_1.2.3.4_43483_3",
"solisconnect_1.2.3.4_43483_4",
Expand All @@ -65,12 +56,6 @@
"solisconnect_1.2.3.4_43707_7",
"solisconnect_1.2.3.4_43707_8",
"solisconnect_1.2.3.4_43707_9",
"solisconnect_1.2.3.4_43815_0",
"solisconnect_1.2.3.4_43815_1",
"solisconnect_1.2.3.4_43815_2",
"solisconnect_1.2.3.4_43815_3",
"solisconnect_1.2.3.4_43815_4",
"solisconnect_1.2.3.4_43815_5",
"solisconnect_1.2.3.4_44280_4",
"solisconnect_1.2.3.4_90005_0",
"solisconnect_1.2.3.4_solisconnect_inverter_a_phase_current",
Expand Down
15 changes: 0 additions & 15 deletions tests/known_sensors_identification.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,7 @@
"solisconnect_ID_SNAPSHOT_43302_4",
"solisconnect_ID_SNAPSHOT_43302_8",
"solisconnect_ID_SNAPSHOT_43302_9",
"solisconnect_ID_SNAPSHOT_43340_0",
"solisconnect_ID_SNAPSHOT_43340_1",
"solisconnect_ID_SNAPSHOT_43363_1",
"solisconnect_ID_SNAPSHOT_43365_0",
"solisconnect_ID_SNAPSHOT_43365_1",
"solisconnect_ID_SNAPSHOT_43365_2",
"solisconnect_ID_SNAPSHOT_43365_3",
"solisconnect_ID_SNAPSHOT_43365_4",
"solisconnect_ID_SNAPSHOT_43483_0",
"solisconnect_ID_SNAPSHOT_43483_1",
"solisconnect_ID_SNAPSHOT_43483_2",
"solisconnect_ID_SNAPSHOT_43483_3",
"solisconnect_ID_SNAPSHOT_43483_4",
Expand All @@ -65,12 +56,6 @@
"solisconnect_ID_SNAPSHOT_43707_7",
"solisconnect_ID_SNAPSHOT_43707_8",
"solisconnect_ID_SNAPSHOT_43707_9",
"solisconnect_ID_SNAPSHOT_43815_0",
"solisconnect_ID_SNAPSHOT_43815_1",
"solisconnect_ID_SNAPSHOT_43815_2",
"solisconnect_ID_SNAPSHOT_43815_3",
"solisconnect_ID_SNAPSHOT_43815_4",
"solisconnect_ID_SNAPSHOT_43815_5",
"solisconnect_ID_SNAPSHOT_44280_4",
"solisconnect_ID_SNAPSHOT_90005_0",
"solisconnect_ID_SNAPSHOT_solisconnect_inverter_a_phase_current",
Expand Down
15 changes: 0 additions & 15 deletions tests/known_sensors_serial.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,7 @@
"solisconnect_SN_SNAPSHOT_43302_4",
"solisconnect_SN_SNAPSHOT_43302_8",
"solisconnect_SN_SNAPSHOT_43302_9",
"solisconnect_SN_SNAPSHOT_43340_0",
"solisconnect_SN_SNAPSHOT_43340_1",
"solisconnect_SN_SNAPSHOT_43363_1",
"solisconnect_SN_SNAPSHOT_43365_0",
"solisconnect_SN_SNAPSHOT_43365_1",
"solisconnect_SN_SNAPSHOT_43365_2",
"solisconnect_SN_SNAPSHOT_43365_3",
"solisconnect_SN_SNAPSHOT_43365_4",
"solisconnect_SN_SNAPSHOT_43483_0",
"solisconnect_SN_SNAPSHOT_43483_1",
"solisconnect_SN_SNAPSHOT_43483_2",
"solisconnect_SN_SNAPSHOT_43483_3",
"solisconnect_SN_SNAPSHOT_43483_4",
Expand All @@ -65,12 +56,6 @@
"solisconnect_SN_SNAPSHOT_43707_7",
"solisconnect_SN_SNAPSHOT_43707_8",
"solisconnect_SN_SNAPSHOT_43707_9",
"solisconnect_SN_SNAPSHOT_43815_0",
"solisconnect_SN_SNAPSHOT_43815_1",
"solisconnect_SN_SNAPSHOT_43815_2",
"solisconnect_SN_SNAPSHOT_43815_3",
"solisconnect_SN_SNAPSHOT_43815_4",
"solisconnect_SN_SNAPSHOT_43815_5",
"solisconnect_SN_SNAPSHOT_44280_4",
"solisconnect_SN_SNAPSHOT_90005_0",
"solisconnect_SN_SNAPSHOT_solisconnect_inverter_a_phase_current",
Expand Down
54 changes: 54 additions & 0 deletions tests/test_ac_coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
InverterOptions,
inverter_options_from_config,
)
from custom_components.solisconnect.sensor_data.switch_sensors import get_switch_sensors


def _switch_names(inverter_config: InverterConfig) -> set[str]:
return {entity["name"] for group in get_switch_sensors(inverter_config) for entity in group["entities"]}


def test_ac_coupling_feature_enabled():
Expand All @@ -22,6 +27,21 @@ def test_ac_coupling_feature_disabled_by_default():
assert InverterFeature.AC_COUPLING not in config.features


def test_generator_feature_disabled_by_default():
"""Generator controls must be opt-in, matching the config-flow default."""
config = InverterConfig(model="S6-EH1P", wattage=[8000], phases=1, type=InverterType.HYBRID)

assert InverterFeature.GENERATOR not in config.features


def test_inverter_options_generator_default_is_false():
template = next(inv for inv in SOLIS_INVERTERS if inv.model == "S6-EH1P")

options = inverter_options_from_config({}, template)

assert options.generator is False


def test_clone_applies_user_options_and_leaves_templates_untouched():
"""User options must rebuild features; SOLIS_INVERTERS entries must stay immutable."""
template = next(inv for inv in SOLIS_INVERTERS if inv.model == "S6-EH1P")
Expand Down Expand Up @@ -55,6 +75,40 @@ def test_hybrid_sensors_ac_coupling_requirement():
assert InverterFeature.AC_COUPLING in group["feature_requirement"]


def test_generator_switches_hidden_without_generator_feature():
template = next(inv for inv in SOLIS_INVERTERS if inv.model == "S6-EH1P")
config = template.clone_with_options(InverterOptions(generator=False, ac_coupling=False), "S2_WL_ST")

names = _switch_names(config)

assert all("Generator" not in name for name in names)
assert "AC Coupling Position (off = GEN port, on = Backup port)" not in names
assert "AC Coupling Enable" not in names


def test_generator_switches_enabled_with_generator_feature():
template = next(inv for inv in SOLIS_INVERTERS if inv.model == "S6-EH1P")
config = template.clone_with_options(InverterOptions(generator=True, ac_coupling=False), "S2_WL_ST")

names = _switch_names(config)

assert "With Generator" in names
assert "Generator Charge Enable" in names
assert "Force Start Generator" in names
assert "AC Coupling Enable" not in names


def test_ac_coupling_switches_enabled_with_ac_coupling_feature():
template = next(inv for inv in SOLIS_INVERTERS if inv.model == "S6-EH1P")
config = template.clone_with_options(InverterOptions(generator=False, ac_coupling=True), "S2_WL_ST")

names = _switch_names(config)

assert "AC Coupling Position (off = GEN port, on = Backup port)" in names
assert "AC Coupling Enable" in names
assert "With Generator" not in names


def test_parallel_feature_disabled_by_default():
"""PARALLEL is opt-in; default installs must not advertise it on the template."""
config = InverterConfig(model="S5-EH1P", wattage=[5000], phases=1, type=InverterType.HYBRID)
Expand Down
Loading
Loading