- SAVE Connect is a gateway device that translates HTTP REST API calls to Modbus RTU
- The
devintegration communicates with SAVE Connect via HTTP (/mread,/mwrite,/menu,/unit_version) - SAVE Connect handles the actual Modbus communication with the ventilation unit
- This is different from
repo2_nonhacswhich uses direct Modbus TCP (via Elfin EW11 converter)
- Models are detected via
unit_version["MB Model"]API endpoint during setup - Available models: VTR-300, VTR-500, VSR-300 (and potentially others)
- Currently, the integration uses a single unified register map for all models
- This needs to change - different models may have different register addresses and available features
-
Communication Method:
- repo2_nonhacs: Direct Modbus TCP (via Elfin EW11 converter)
- dev: HTTP API via SAVE Connect gateway
-
Register Addressing:
- repo2_nonhacs: Uses Modbus addresses directly (may be 0-indexed or 1-indexed)
- dev: Uses register numbers that SAVE Connect translates (register - 1 in API calls)
- Important: Register address discrepancies may be due to:
- Model differences (VTR-500 vs VTR-300 vs VSR-300)
- Direct Modbus vs SAVE Connect API translation
- Different firmware versions
Priority: HIGH
Create a model detection and register mapping system:
# In modbus.py or new file models.py
from enum import Enum
class SystemairModel(Enum):
VTR300 = "VTR-300"
VTR500 = "VTR-500"
VSR300 = "VSR-300"
UNKNOWN = "Unknown"
# Model-specific parameter maps
MODEL_PARAMETER_MAPS = {
SystemairModel.VTR300: {...},
SystemairModel.VTR500: {...},
SystemairModel.VSR300: {...},
}Implementation Steps:
- Extract model from
mb_modelfield inSystemairData - Create model-specific parameter maps
- Update
parameter_mapto be model-aware - Add fallback to common registers for unknown models
Priority: HIGH
The register discrepancies found need model-specific verification:
| Register Name | dev (current) | repo2_nonhacs | Likely Model | Notes |
|---|---|---|---|---|
| Mode Status | 1161 (Input) | 1160 (Holding) | VTR-500 | May be model-specific |
| Temp Setpoint | 2001 | 2000 | VTR-500 | Verify for each model |
| Overheat Temp | 12108 | 12107 | VTR-500 | Different sensors? |
| Alarm A/B/C | 15901-15903 | 15900-15902 | VTR-500 | Offset difference |
| Filter Time | 7005/7006 (32-bit) | 7005/7006 (alarm) | VTR-500 | Different interpretation |
Action Items:
- Test register reads for each model (VTR-300, VTR-500, VSR-300)
- Document which registers exist on which models
- Create model-specific register maps
- Handle missing registers gracefully (some may not exist on all models)
Priority: HIGH
Some features may only be available on certain models:
# In sensor.py, switch.py, etc.
def should_create_entity(model: SystemairModel, register: ModbusParameter) -> bool:
"""Check if entity should be created for this model."""
model_features = MODEL_FEATURES.get(model, set())
return register.short in model_featuresFeatures to check:
- Heater support (VTR-500 has heater, VSR-300 may not)
- Cooler support (model-dependent)
- Specific alarm types (may vary by model)
- Advanced features (eco mode, free cooling, etc.)
Priority: MEDIUM
Create model-specific sensor entity descriptions:
# In sensor.py
def get_sensor_entities_for_model(model: SystemairModel) -> tuple:
"""Get sensor entities available for specific model."""
base_sensors = BASE_SENSOR_ENTITIES
model_specific = MODEL_SPECIFIC_SENSORS.get(model, [])
return base_sensors + model_specificThese should work across all models:
- Efficiency Temperature (register 12106) - Verify per model
- Calculated Moisture Extraction (register 2210) - Verify per model
- Calculated Moisture Intake (register 2211) - Verify per model
- Supply Air Fan Power Factor (register 14000) - Verify per model
- Extractor Fan Power Factor (register 14001) - Verify per model
- Heat Recovery (register 14102) - Verify per model
These are likely VTR-500 specific:
- Summer/Winter Operation (register 1038)
- Extractor Hood Pressure Switch (register 12020)
- TRIAC After Manual Override (register 2148)
- TRIAC Control Signal (register 14380)
- Manual Mode Command Register (register 1130)
- Exhaust Air Setpoint (register 2012)
- Exhaust Air Min/Max Setpoint (registers 2020, 2021)
- Temperature Control Mode (register 2030)
- Eco Heat Offset (register 2503)
- Fan Speed Compensation registers (1251-1258)
- Filter Replacement Period (register 7000)
- Moisture Extraction Setpoint (register 2202)
Implementation: Add with model check - only create for VTR-500
Action: Review documentation PDFs to identify model-specific registers
- May have different register addresses
- May have different feature sets
- May lack some VTR-500 features
Priority: HIGH
# In modbus.py
COMMON_PARAMETERS = [
# Registers that exist on all models with same address
ModbusParameter(register=12102, ...), # OAT - common
ModbusParameter(register=12103, ...), # SAT - common
# ...
]
VTR500_SPECIFIC_PARAMETERS = [
# VTR-500 specific registers
ModbusParameter(register=1038, ...), # Summer/Winter
ModbusParameter(register=2503, ...), # Eco offset
# ...
]
VTR300_SPECIFIC_PARAMETERS = [
# VTR-300 specific registers (to be determined from docs)
# ...
]
VSR300_SPECIFIC_PARAMETERS = [
# VSR-300 specific registers (to be determined from docs)
# ...
]
def get_parameters_for_model(model: SystemairModel) -> dict:
"""Get parameter map for specific model."""
params = COMMON_PARAMETERS.copy()
if model == SystemairModel.VTR500:
params.extend(VTR500_SPECIFIC_PARAMETERS)
elif model == SystemairModel.VTR300:
params.extend(VTR300_SPECIFIC_PARAMETERS)
elif model == SystemairModel.VSR300:
params.extend(VSR300_SPECIFIC_PARAMETERS)
return {param.short: param for param in params}Create a mapping table to resolve discrepancies:
# Register address mappings by model
REGISTER_MAPPINGS = {
SystemairModel.VTR500: {
"REG_USERMODE_MODE": 1160, # repo2_nonhacs uses 1160
"REG_TC_SP": 2000, # repo2_nonhacs uses 2000
# ...
},
SystemairModel.VTR300: {
"REG_USERMODE_MODE": 1161, # dev uses 1161
"REG_TC_SP": 2001, # dev uses 2001
# ...
},
# ...
}Priority: HIGH
Combine mode status register with manual command register for detailed status:
- "Auto schedule - Low" vs "Auto schedule - Normal" vs "Auto schedule - High"
- "Manual Low" vs "Manual Normal" vs "Manual High"
- Better mode status display
Implementation:
- Add
REG_USERMODE_MANUAL_COMMAND(register 1130) to modbus.py - Update climate entity to combine both registers
- Model-specific: Verify register exists on all models
Priority: MEDIUM
Display remaining time for active modes (Away, Crowded, Refresh, Fireplace, Holiday).
Implementation:
- Use existing registers 1111/1112 (32-bit remaining time)
- Add template sensors or calculate in Python
- Format as "X h Y min" or "X days"
- Model-specific: Verify countdown works on all models
Priority: MEDIUM
- Supply Air Flow Rate:
(fan_power_factor * 3) rounded(m³/h) - Exhaust Air Flow Rate:
(fan_power_factor * 3) rounded(m³/h) - Recovery Rate:
((supply_temp - outdoor_temp) / (exhaust_temp - outdoor_temp)) * 100(%) - Time to Filter Change: Format seconds as months/days
- Mode Status: Combine registers for detailed status
- Summer/Winter Status: Convert 0/1 to "Summer"/"Winter"
- Regulation Mode: Convert 0/1/2 to descriptive text
Implementation Options:
- Python calculations in sensor.py (better performance)
- Template sensors in YAML (more flexible, user-customizable)
- Hybrid: Core calculations in Python, advanced in templates
Priority: MEDIUM
- Eco Heat Offset (register 2503) - VTR-500 specific
- Filter Replacement Period (register 7000) - Verify per model
- Moisture Extraction Setpoint (register 2202) - Verify per model
- Fan Speed Normal (registers 1414, 1415) - If configurable
Priority: HIGH
# In coordinator.py
class SystemairDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant) -> None:
super().__init__(...)
self.model: SystemairModel = SystemairModel.UNKNOWN
self.parameter_map: dict[str, ModbusParameter] = {}
async def _async_setup(self) -> None:
# ... existing code ...
model_str = self.config_entry.runtime_data.mb_model
self.model = SystemairModel(model_str) if model_str else SystemairModel.UNKNOWN
self.parameter_map = get_parameters_for_model(self.model)Priority: HIGH
# In sensor.py, switch.py, etc.
async def async_setup_entry(
hass: HomeAssistant,
entry: SystemairConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator = entry.runtime_data.coordinator
model = coordinator.model
# Get model-specific entity descriptions
entity_descriptions = get_entity_descriptions_for_model(model)
async_add_entities(
SystemairSensor(coordinator, desc)
for desc in entity_descriptions
if should_create_entity(model, desc.registry)
)Priority: HIGH
# In coordinator.py
def get_modbus_data(self, register: ModbusParameter) -> float:
"""Get the data for a Modbus register."""
# Check if register exists for this model
if register.short not in self.parameter_map:
LOGGER.debug("Register %s not available for model %s", register.short, self.model)
return None # or 0, depending on use case
# ... existing code ...Priority: LOW
- Document which features are available on which models
- Create model comparison table
- Document register differences between models
Priority: LOW
- Port Lovelace configs as examples (not part of integration)
- Create model-specific dashboard examples
- Document customization options
Priority: LOW
- Port Node-RED flows as examples
- Document automation patterns
- Model-specific automation examples
- Test with VTR-300 unit
- Test with VTR-500 unit
- Test with VSR-300 unit
- Test with unknown/unsupported model (graceful fallback)
- Verify common registers work on all models
- Verify model-specific registers only appear on correct models
- Test register address mappings
- Test missing register handling
- Test all sensors on each model
- Test all switches on each model
- Test all number entities on each model
- Test climate entity on each model
- Test calculated/template sensors
- Ensure existing installations continue to work
- Test migration from unified to model-specific maps
- Verify default behavior for unknown models
- Model Detection System - Extract and store model from API
- Model-Specific Parameter Maps - Create model registry system
- Register Address Verification - Test registers on each model
- Graceful Missing Register Handling - Don't crash on missing registers
- VTR-500 Specific Sensors - Port from repo2_nonhacs with model checks
- Enhanced Mode Detection - Combine registers for better status
- Template Sensors - Calculated values (flow rates, recovery rate)
- Additional Number Entities - Eco offset, filter period, etc.
- Countdown Timer Support - Remaining time for active modes
- VTR-300 / VSR-300 Specific Features - From documentation
- Model-Specific Documentation - Feature comparison table
- Lovelace Dashboard Examples - User customization examples
- Node-RED Flow Examples - Automation examples
- Advanced Configuration Options - User preferences
- SAVE Connect is a gateway - Not direct Modbus communication
- Model detection is available - Via
unit_version["MB Model"]API - Register addresses may differ by model - Need model-specific maps
- Differences between dev and repo2_nonhacs are likely due to:
- Model differences - VTR-500 vs VTR-300 vs VSR-300
- Communication method - Direct Modbus vs SAVE Connect API
- Firmware versions - Different versions may use different registers
- Must implement model-aware system first - Before adding new registers
- Test registers per model - Cannot assume all registers exist on all models
- Graceful degradation - Handle missing features/registers gracefully
- Extract model information during setup
- Create model-specific register maps
- Conditional entity creation based on model
- Documentation of model differences
- Extract Model Information - Update coordinator to store and use model
- Create Model Registry - Define model enum and parameter maps
- Test Register Availability - Test common registers on each model
- Implement Model-Specific Maps - Create VTR-500, VTR-300, VSR-300 maps
- Port VTR-500 Features - Add repo2_nonhacs features with model checks
- Document Model Differences - Create comparison table
- Test All Models - Verify functionality on each supported model
- SAVE Connect handles Modbus translation, so register addresses in API may differ from direct Modbus
- Some registers may be read-only or write-only depending on model
- Firmware versions may affect register availability
- Always test register reads/writes before assuming they work
- Provide fallback behavior for unknown models or missing registers
- VSR-300 features can be fully tested and verified
- VTR-500 specific features from repo2_nonhacs cannot be tested
- VTR-300 features cannot be tested
- Register addresses for VTR-500/VTR-300 should be verified when hardware becomes available
- VTR-500 specific sensors should be marked as "untested" or "VTR-500 only" in code comments