Skip to content

Commit f17dee2

Browse files
committed
+ Added model selection to integration
+ Added power consumption sensor for fans & heater
1 parent 72457d9 commit f17dee2

File tree

7 files changed

+205
-25
lines changed

7 files changed

+205
-25
lines changed

custom_components/systemair/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from homeassistant.loader import async_get_loaded_integration
99

1010
from .api import SystemairVSRModbusClient
11-
from .const import CONF_SLAVE_ID
11+
from .const import CONF_MODEL, CONF_SLAVE_ID
1212
from .coordinator import SystemairDataUpdateCoordinator
1313
from .data import SystemairConfigEntry, SystemairData
1414

@@ -35,16 +35,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: SystemairConfigEntry) ->
3535
slave_id=entry.data[CONF_SLAVE_ID],
3636
)
3737

38-
coordinator = SystemairDataUpdateCoordinator(hass=hass, client=client)
38+
coordinator = SystemairDataUpdateCoordinator(
39+
hass=hass, client=client, config_entry=entry
40+
)
41+
42+
model = entry.options.get(CONF_MODEL, entry.data.get(CONF_MODEL, "VSR 300"))
3943

4044
entry.runtime_data = SystemairData(
4145
client=client,
4246
coordinator=coordinator,
4347
integration=async_get_loaded_integration(hass, entry.domain),
48+
model=model,
4449
)
4550

46-
await coordinator.async_config_entry_first_refresh()
47-
4851
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
4952
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
5053

custom_components/systemair/config_flow.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,34 @@
55
import voluptuous as vol
66
from homeassistant import config_entries
77
from homeassistant.const import CONF_HOST, CONF_PORT
8+
from homeassistant.core import callback
89
from homeassistant.helpers import selector
910

1011
from .api import ModbusConnectionError, SystemairVSRModbusClient
11-
from .const import CONF_SLAVE_ID, DEFAULT_PORT, DEFAULT_SLAVE_ID, DOMAIN, LOGGER
12+
from .const import (
13+
CONF_MODEL,
14+
CONF_SLAVE_ID,
15+
DEFAULT_PORT,
16+
DEFAULT_SLAVE_ID,
17+
DOMAIN,
18+
LOGGER,
19+
MODEL_SPECS,
20+
)
1221

1322

1423
class SystemairVSRConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
1524
"""Handle a config flow for Systemair VSR."""
1625

1726
VERSION = 1
1827

28+
@staticmethod
29+
@callback
30+
def async_get_options_flow(
31+
config_entry: config_entries.ConfigEntry,
32+
) -> SystemairOptionsFlowHandler:
33+
"""Get the options flow for this handler."""
34+
return SystemairOptionsFlowHandler(config_entry)
35+
1936
async def _validate_connection(self, user_input: dict) -> None:
2037
"""Validate the connection to the VSR unit."""
2138
client = SystemairVSRModbusClient(
@@ -53,7 +70,44 @@ async def async_step_user(self, user_input: dict | None = None) -> config_entrie
5370
vol.Required(CONF_HOST): selector.TextSelector(),
5471
vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
5572
vol.Required(CONF_SLAVE_ID, default=DEFAULT_SLAVE_ID): vol.Coerce(int),
73+
vol.Required(CONF_MODEL): selector.SelectSelector(
74+
selector.SelectSelectorConfig(
75+
options=list(MODEL_SPECS.keys()),
76+
mode=selector.SelectSelectorMode.DROPDOWN,
77+
)
78+
),
5679
}
5780
),
5881
errors=errors,
5982
)
83+
84+
85+
class SystemairOptionsFlowHandler(config_entries.OptionsFlow):
86+
"""Handle an options flow for Systemair."""
87+
88+
async def async_step_init(
89+
self, user_input: dict | None = None
90+
) -> config_entries.ConfigFlowResult:
91+
"""Manage the options."""
92+
if user_input is not None:
93+
return self.async_create_entry(title="", data=user_input)
94+
95+
default_model = self.config_entry.options.get(
96+
CONF_MODEL, self.config_entry.data.get(CONF_MODEL, "VSR 300")
97+
)
98+
99+
return self.async_show_form(
100+
step_id="init",
101+
data_schema=vol.Schema(
102+
{
103+
vol.Required(
104+
CONF_MODEL, default=default_model
105+
): selector.SelectSelector(
106+
selector.SelectSelectorConfig(
107+
options=list(MODEL_SPECS.keys()),
108+
mode=selector.SelectSelectorMode.DROPDOWN,
109+
)
110+
),
111+
}
112+
),
113+
)

custom_components/systemair/const.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@
77
DOMAIN = "systemair"
88
ATTRIBUTION = "Data provided by Systemair SAVE Connect."
99

10-
# Constants for Modbus configuration
10+
# --- Configuration Constants ---
11+
CONF_MODEL = "model"
1112
CONF_SLAVE_ID = "slave_id"
1213
DEFAULT_PORT = 502
1314
DEFAULT_SLAVE_ID = 1
1415

16+
# --- Power Specs for different models ---
17+
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},
23+
}
24+
25+
1526
# Constants from the old integration
1627
MAX_TEMP = 30
1728
MIN_TEMP = 12
@@ -22,4 +33,4 @@
2233
PRESET_MODE_REFRESH = "refresh"
2334
PRESET_MODE_FIREPLACE = "fireplace"
2435
PRESET_MODE_AWAY = "away"
25-
PRESET_MODE_HOLIDAY = "holiday"
36+
PRESET_MODE_HOLIDAY = "holiday"

custom_components/systemair/coordinator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ def __init__(
3636
self,
3737
hass: HomeAssistant,
3838
client: SystemairVSRModbusClient,
39+
config_entry: SystemairConfigEntry,
3940
) -> None:
4041
"""Initialize."""
4142
self.client = client
43+
self.config_entry = config_entry
4244
super().__init__(
4345
hass=hass,
4446
logger=LOGGER,
@@ -94,4 +96,4 @@ async def _async_update_data(self) -> dict[str, Any]:
9496
try:
9597
return await self.client.get_all_data()
9698
except ModbusConnectionError as exception:
97-
raise UpdateFailed(exception) from exception
99+
raise UpdateFailed(exception) from exception

custom_components/systemair/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class SystemairData:
2323
client: SystemairVSRModbusClient
2424
coordinator: SystemairDataUpdateCoordinator
2525
integration: Integration
26+
model: str
2627

2728
iam_sw_version: str | None = None
2829
mb_hw_version: str | None = None

custom_components/systemair/sensor.py

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
1-
"""Sensor platform for Systemair0."""
1+
"""Sensor platform for Systemair."""
22

33
from __future__ import annotations
44

55
from dataclasses import dataclass
66
from typing import TYPE_CHECKING
77

88
from homeassistant.components.sensor import (
9+
SensorDeviceClass,
910
SensorEntity,
1011
SensorEntityDescription,
12+
SensorStateClass,
13+
)
14+
from homeassistant.const import (
15+
PERCENTAGE,
16+
REVOLUTIONS_PER_MINUTE,
17+
EntityCategory,
18+
UnitOfPower,
19+
UnitOfTemperature,
20+
UnitOfTime,
1121
)
12-
from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass
13-
from homeassistant.const import PERCENTAGE, REVOLUTIONS_PER_MINUTE, EntityCategory, UnitOfTemperature, UnitOfTime
1422

23+
from .const import MODEL_SPECS
1524
from .entity import SystemairEntity
1625
from .modbus import ModbusParameter, alarm_parameters, parameter_map
1726

@@ -28,9 +37,7 @@
2837
"Waiting": 2,
2938
"Cleared Error Active": 3,
3039
}
31-
3240
VALUE_MAP_TO_ALARM_STATE = {value: key for key, value in ALARM_STATE_TO_VALUE_MAP.items()}
33-
3441
IAQ_LEVEL_MAP = {0: "Perfect", 1: "Good", 2: "Improving"}
3542
DEMAND_CONTROLLER_MAP = {0: "CO2", 1: "RH"}
3643
DEFROSTING_STATE_MAP = {
@@ -45,10 +52,38 @@
4552
@dataclass(kw_only=True, frozen=True)
4653
class SystemairSensorEntityDescription(SensorEntityDescription):
4754
"""Describes a Systemair sensor entity."""
48-
4955
registry: ModbusParameter
5056

5157

58+
@dataclass(kw_only=True, frozen=True)
59+
class SystemairPowerSensorEntityDescription(SensorEntityDescription):
60+
"""Describes a Systemair power sensor entity."""
61+
62+
63+
POWER_SENSORS: tuple[SystemairPowerSensorEntityDescription, ...] = (
64+
SystemairPowerSensorEntityDescription(
65+
key="supply_fan_power",
66+
translation_key="supply_fan_power",
67+
device_class=SensorDeviceClass.POWER,
68+
native_unit_of_measurement=UnitOfPower.WATT,
69+
state_class=SensorStateClass.MEASUREMENT,
70+
),
71+
SystemairPowerSensorEntityDescription(
72+
key="extract_fan_power",
73+
translation_key="extract_fan_power",
74+
device_class=SensorDeviceClass.POWER,
75+
native_unit_of_measurement=UnitOfPower.WATT,
76+
state_class=SensorStateClass.MEASUREMENT,
77+
),
78+
SystemairPowerSensorEntityDescription(
79+
key="total_power",
80+
translation_key="total_power",
81+
device_class=SensorDeviceClass.POWER,
82+
native_unit_of_measurement=UnitOfPower.WATT,
83+
state_class=SensorStateClass.MEASUREMENT,
84+
),
85+
)
86+
5287
ENTITY_DESCRIPTIONS = (
5388
SystemairSensorEntityDescription(
5489
key="outside_air_temperature",
@@ -170,23 +205,27 @@ class SystemairSensorEntityDescription(SensorEntityDescription):
170205

171206

172207
async def async_setup_entry(
173-
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
208+
hass: HomeAssistant, # noqa: ARG001
174209
entry: SystemairConfigEntry,
175210
async_add_entities: AddEntitiesCallback,
176211
) -> None:
177212
"""Set up the sensor platform."""
178-
async_add_entities(
179-
SystemairSensor(
180-
coordinator=entry.runtime_data.coordinator,
181-
entity_description=entity_description,
182-
)
183-
for entity_description in ENTITY_DESCRIPTIONS
184-
)
213+
coordinator = entry.runtime_data.coordinator
214+
215+
sensors = [
216+
SystemairSensor(coordinator=coordinator, entity_description=desc)
217+
for desc in ENTITY_DESCRIPTIONS
218+
]
219+
power_sensors = [
220+
SystemairPowerSensor(coordinator=coordinator, entity_description=desc)
221+
for desc in POWER_SENSORS
222+
]
223+
224+
async_add_entities(sensors + power_sensors)
185225

186226

187227
class SystemairSensor(SystemairEntity, SensorEntity):
188228
"""Systemair Sensor class."""
189-
190229
_attr_has_entity_name = True
191230

192231
entity_description: SystemairSensorEntityDescription
@@ -218,4 +257,54 @@ def native_value(self) -> str | None:
218257
if self.device_class == SensorDeviceClass.ENUM:
219258
return VALUE_MAP_TO_ALARM_STATE.get(value, "Inactive")
220259

221-
return str(value)
260+
return str(value)
261+
262+
263+
class SystemairPowerSensor(SystemairEntity, SensorEntity):
264+
"""Systemair Power Sensor class for calculated values."""
265+
266+
_attr_has_entity_name = True
267+
entity_description: SystemairPowerSensorEntityDescription
268+
269+
def __init__(
270+
self,
271+
coordinator: SystemairDataUpdateCoordinator,
272+
entity_description: SystemairPowerSensorEntityDescription,
273+
) -> None:
274+
"""Initialize the power sensor class."""
275+
super().__init__(coordinator)
276+
self.entity_description = entity_description
277+
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{entity_description.key}"
278+
279+
@property
280+
def native_value(self) -> float | None:
281+
"""Return the calculated power consumption."""
282+
model = self.coordinator.config_entry.runtime_data.model
283+
specs = MODEL_SPECS.get(model)
284+
if not specs:
285+
return None
286+
287+
# Get current fan speeds and heater status
288+
supply_fan_pct = self.coordinator.get_modbus_data(parameter_map["REG_OUTPUT_SAF"])
289+
extract_fan_pct = self.coordinator.get_modbus_data(parameter_map["REG_OUTPUT_EAF"])
290+
heater_on = self.coordinator.get_modbus_data(parameter_map["REG_OUTPUT_TRIAC"])
291+
292+
if supply_fan_pct is None or extract_fan_pct is None or heater_on is None:
293+
return None
294+
295+
# Assume max fan power is split 50/50 between supply and extract fans
296+
max_fan_power_per_fan = specs["fan_power"] / 2
297+
298+
supply_power = (supply_fan_pct / 100) * max_fan_power_per_fan
299+
extract_power = (extract_fan_pct / 100) * max_fan_power_per_fan
300+
heater_power = specs["heater_power"] if heater_on else 0
301+
302+
key = self.entity_description.key
303+
if key == "supply_fan_power":
304+
return round(supply_power, 1)
305+
if key == "extract_fan_power":
306+
return round(extract_power, 1)
307+
if key == "total_power":
308+
return round(supply_power + extract_power + heater_power, 1)
309+
310+
return None

custom_components/systemair/translations/en.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"user": {
55
"description": "You need enter IAM Module IP Address",
66
"data": {
7-
"ip_address": "IP Address"
7+
"ip_address": "IP Address",
8+
"model": "Ventilation Unit Model"
89
}
910
}
1011
},
@@ -14,6 +15,16 @@
1415
"already_configured": "This unit is already configured."
1516
}
1617
},
18+
"options": {
19+
"step": {
20+
"init": {
21+
"title": "Systemair Settings",
22+
"data": {
23+
"model": "Ventilation Unit Model"
24+
}
25+
}
26+
}
27+
},
1728
"entity": {
1829
"binary_sensor": {
1930
"heat_exchange_active": {
@@ -99,6 +110,15 @@
99110
},
100111
"defrosting_state": {
101112
"name": "Defrosting State"
113+
},
114+
"supply_fan_power": {
115+
"name": "Supply Fan Power"
116+
},
117+
"extract_fan_power": {
118+
"name": "Extract Fan Power"
119+
},
120+
"total_power": {
121+
"name": "Total Power Consumption"
102122
}
103123
},
104124
"switch": {

0 commit comments

Comments
 (0)