Skip to content

Commit 1b78abc

Browse files
committed
+ Added all SAVE units information for calculate power consumption
* Fixed changing unit model cause to crash + Added model images for future use * Cleanup & Stabilize
1 parent c195b0e commit 1b78abc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+273
-26
lines changed

custom_components/systemair/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: SystemairConfigEntry) ->
4747
model=model,
4848
)
4949

50+
entry.async_on_unload(entry.add_update_listener(async_options_update_listener))
51+
5052
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
51-
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
5253

5354
return True
5455

5556

57+
async def async_options_update_listener(_hass: HomeAssistant, entry: ConfigEntry) -> None:
58+
"""Handle options update."""
59+
entry.runtime_data.model = entry.options[CONF_MODEL]
60+
61+
5662
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
5763
"""Handle removal of an entry."""
5864
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

custom_components/systemair/api.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pymodbus.exceptions import ConnectionException
99

1010
from .const import LOGGER
11+
from .modbus import parameter_map
1112

1213
MODBUS_DEVICE_BUSY_EXCEPTION = 6
1314
MODBUS_GATEWAY_TARGET_FAILED_TO_RESPOND = 11
@@ -18,7 +19,7 @@ class ModbusConnectionError(Exception):
1819

1920

2021
class SystemairVSRModbusClient:
21-
"""Provides a client for interacting with a Systemair VSR unit via Modbus."""
22+
"""Provides a client for interacting with a Systemair VSR unit."""
2223

2324
def __init__(self, host: str, port: int, slave_id: int, timeout: int = 5) -> None:
2425
"""Initialize the Modbus client."""
@@ -55,6 +56,20 @@ async def stop(self) -> None:
5556
await self._close_connection()
5657
LOGGER.info("Systemair Modbus client worker stopped.")
5758

59+
async def test_connection(self) -> bool:
60+
"""Start, test a single read, and stop the client to validate connection."""
61+
try:
62+
await self.start()
63+
test_register_1based = parameter_map["REG_TC_SP"].register
64+
await self._queue_request("read", address=test_register_1based - 1, count=1)
65+
except (TimeoutError, ModbusConnectionError) as e:
66+
LOGGER.error("Failed to connect during test: %s", e)
67+
return False
68+
else:
69+
return True
70+
finally:
71+
await self.stop()
72+
5873
async def _ensure_connected(self) -> None:
5974
"""Ensure the client is connected, establishing connection if needed."""
6075
if self._client and self._client.connected:

custom_components/systemair/config_flow.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ class SystemairVSRConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
2828
@staticmethod
2929
@callback
3030
def async_get_options_flow(
31-
config_entry: config_entries.ConfigEntry,
31+
_config_entry: config_entries.ConfigEntry,
3232
) -> SystemairOptionsFlowHandler:
3333
"""Get the options flow for this handler."""
34-
return SystemairOptionsFlowHandler(config_entry)
34+
return SystemairOptionsFlowHandler()
3535

3636
async def _validate_connection(self, user_input: dict) -> None:
3737
"""Validate the connection to the VSR unit."""
@@ -55,7 +55,7 @@ async def async_step_user(self, user_input: dict | None = None) -> config_entrie
5555
errors["base"] = "cannot_connect"
5656
except (TimeoutError, OSError) as e:
5757
LOGGER.exception("Unexpected exception: %s", e)
58-
errors["base"] = "cannot_connect"
58+
errors["base"] = "unknown"
5959
else:
6060
unique_id = f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
6161
await self.async_set_unique_id(unique_id)
@@ -70,7 +70,7 @@ async def async_step_user(self, user_input: dict | None = None) -> config_entrie
7070
vol.Required(CONF_HOST): selector.TextSelector(),
7171
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
7272
vol.Required(CONF_SLAVE_ID, default=DEFAULT_SLAVE_ID): vol.Coerce(int),
73-
vol.Required(CONF_MODEL): selector.SelectSelector(
73+
vol.Required(CONF_MODEL, default=next(iter(MODEL_SPECS))): selector.SelectSelector(
7474
selector.SelectSelectorConfig(
7575
options=list(MODEL_SPECS.keys()),
7676
mode=selector.SelectSelectorMode.DROPDOWN,
@@ -85,6 +85,11 @@ async def async_step_user(self, user_input: dict | None = None) -> config_entrie
8585
class SystemairOptionsFlowHandler(config_entries.OptionsFlow):
8686
"""Handle an options flow for Systemair."""
8787

88+
@property
89+
def config_entry(self) -> config_entries.ConfigEntry:
90+
"""Return the config entry for this flow."""
91+
return self.hass.config_entries.async_get_entry(self.handler)
92+
8893
async def async_step_init(self, user_input: dict | None = None) -> config_entries.ConfigFlowResult:
8994
"""Manage the options."""
9095
if user_input is not None:

custom_components/systemair/const.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,53 @@
1515

1616
# --- Power Specs for different models ---
1717
MODEL_SPECS = {
18-
"VSR 300": {"fan_power": 166, "heater_power": 1670},
19-
"VSR 500": {"fan_power": 338, "heater_power": 1670},
20-
"VSR 150/B": {"fan_power": 74, "heater_power": 500},
21-
"VTR 200/B (500Wt Heater)": {"fan_power": 168, "heater_power": 500},
22-
"VTR 200/B (1000Wt Heater)": {"fan_power": 168, "heater_power": 1000},
18+
"VSC 100": {"fan_power": 27, "heater_power": 0, "supply_fans": 1, "extract_fans": 0},
19+
"VSC 200": {"fan_power": 81, "heater_power": 0, "supply_fans": 1, "extract_fans": 0},
20+
"VSC 300": {"fan_power": 115, "heater_power": 0, "supply_fans": 1, "extract_fans": 0},
21+
"VSR 150/B": {"fan_power": 70, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
22+
"VSR 150/B L": {"fan_power": 70, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
23+
"VSR 150/B R": {"fan_power": 70, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
24+
"VSR 200/B L": {"fan_power": 162, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
25+
"VSR 200/B R": {"fan_power": 162, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
26+
"VSR 300": {"fan_power": 166, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
27+
"VSR 400": {"fan_power": 338, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
28+
"VSR 500": {"fan_power": 338, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
29+
"VSR 700": {"fan_power": 340, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
30+
"VTC 200 L": {"fan_power": 170, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
31+
"VTC 200 R": {"fan_power": 170, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
32+
"VTC 200-1 L": {"fan_power": 162, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
33+
"VTC 200-1 R": {"fan_power": 162, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
34+
"VTC 300 L": {"fan_power": 170, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
35+
"VTC 300 R": {"fan_power": 170, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
36+
"VTC 500 L": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
37+
"VTC 500 R": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
38+
"VTC 700 L": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
39+
"VTC 700 R": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
40+
"VTR 100/B": {"fan_power": 70, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
41+
"VTR 150/B L 500W": {"fan_power": 166, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
42+
"VTR 150/B L 1000W": {"fan_power": 166, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
43+
"VTR 150/B R 500W": {"fan_power": 166, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
44+
"VTR 150/B R 1000W": {"fan_power": 166, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
45+
"VTR 150/K L 500W": {"fan_power": 172, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
46+
"VTR 150/K L 1000W": {"fan_power": 172, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
47+
"VTR 150/K R 500W": {"fan_power": 172, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
48+
"VTR 150/K R 1000W": {"fan_power": 172, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
49+
"VTR 250/B L 500W": {"fan_power": 162, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
50+
"VTR 250/B L 1000W": {"fan_power": 162, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
51+
"VTR 250/B R 500W": {"fan_power": 162, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
52+
"VTR 250/B R 1000W": {"fan_power": 162, "heater_power": 1000, "supply_fans": 1, "extract_fans": 1},
53+
"VTR 275/B L": {"fan_power": 162, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
54+
"VTR 275/B R": {"fan_power": 162, "heater_power": 500, "supply_fans": 1, "extract_fans": 1},
55+
"VTR 300/B L": {"fan_power": 162, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
56+
"VTR 300/B R": {"fan_power": 162, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
57+
"VTR 350/B L": {"fan_power": 338, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
58+
"VTR 350/B R": {"fan_power": 338, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
59+
"VTR 500 L": {"fan_power": 340, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
60+
"VTR 500 R": {"fan_power": 340, "heater_power": 1670, "supply_fans": 1, "extract_fans": 1},
61+
"VTR 700 L": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
62+
"VTR 700 R": {"fan_power": 340, "heater_power": 0, "supply_fans": 1, "extract_fans": 1},
2363
}
2464

25-
2665
# Constants from the old integration
2766
MAX_TEMP = 30
2867
MIN_TEMP = 12

custom_components/systemair/coordinator.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(
4141
"""Initialize."""
4242
self.client = client
4343
self.config_entry = config_entry
44+
4445
super().__init__(
4546
hass=hass,
4647
logger=LOGGER,

custom_components/systemair/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
"requirements": [
1414
"pymodbus>=3.11.1"
1515
],
16-
"version": "1.0.3"
17-
}
16+
"version": "1.0.4"
17+
}

custom_components/systemair/sensor.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ class SystemairSensor(SystemairEntity, SensorEntity):
223223
"""Systemair Sensor class."""
224224

225225
_attr_has_entity_name = True
226-
227226
entity_description: SystemairSensorEntityDescription
228227

229228
def __init__(
@@ -280,6 +279,16 @@ def native_value(self) -> float | None:
280279
if not specs:
281280
return None
282281

282+
# Get fan counts from specs, defaulting to 0 if not present
283+
num_supply_fans = specs.get("supply_fans", 0)
284+
num_extract_fans = specs.get("extract_fans", 0)
285+
total_fans = num_supply_fans + num_extract_fans
286+
287+
# Calculate power per fan
288+
power_per_fan = 0
289+
if total_fans > 0:
290+
power_per_fan = specs.get("fan_power", 0) / total_fans
291+
283292
# Get current fan speeds and heater status
284293
supply_fan_pct = self.coordinator.get_modbus_data(parameter_map["REG_OUTPUT_SAF"])
285294
extract_fan_pct = self.coordinator.get_modbus_data(parameter_map["REG_OUTPUT_EAF"])
@@ -288,13 +297,12 @@ def native_value(self) -> float | None:
288297
if supply_fan_pct is None or extract_fan_pct is None or heater_on is None:
289298
return None
290299

291-
# Assume max fan power is split 50/50 between supply and extract fans
292-
max_fan_power_per_fan = specs["fan_power"] / 2
293-
294-
supply_power = (supply_fan_pct / 100) * max_fan_power_per_fan
295-
extract_power = (extract_fan_pct / 100) * max_fan_power_per_fan
296-
heater_power = specs["heater_power"] if heater_on else 0
300+
# Calculate power for each component
301+
supply_power = (supply_fan_pct / 100) * power_per_fan * num_supply_fans
302+
extract_power = (extract_fan_pct / 100) * power_per_fan * num_extract_fans
303+
heater_power = specs.get("heater_power", 0) if heater_on else 0
297304

305+
# Return the correct value based on the sensor's key
298306
key = self.entity_description.key
299307
if key == "supply_fan_power":
300308
return round(supply_power, 1)

custom_components/systemair/translations/en.json

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@
22
"config": {
33
"step": {
44
"user": {
5-
"description": "You need enter IAM Module IP Address",
5+
"title": "Connect to Systemair Unit",
6+
"description": "Enter the connection details for your Systemair IAM module. Press Submit to check the connection.",
7+
"data": {
8+
"host": "IP Address",
9+
"port": "Port",
10+
"slave_id": "Slave ID"
11+
}
12+
},
13+
"model": {
14+
"title": "Select Your Unit Model",
15+
"description": "Choose your ventilation unit model. This is required for accurate power consumption calculations.",
616
"data": {
7-
"ip_address": "IP Address",
817
"model": "Ventilation Unit Model"
918
}
1019
}
1120
},
1221
"error": {
13-
"connection": "Unable to connect to the server.",
14-
"unknown": "Unknown error occurred.",
15-
"already_configured": "This unit is already configured."
22+
"cannot_connect": "Unable to connect to the device. Please check the IP address, port, and that the device is online.",
23+
"unknown": "An unknown error occurred.",
24+
"already_configured": "This device is already configured."
1625
}
1726
},
1827
"options": {

0 commit comments

Comments
 (0)